Fix two crashes when reading invalid pax headers
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 889d312..402f3d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.0.3
+
+- Fix the reader throwing a `FormatException` (instead of a `TarException`)
+  when reading tar files with invalid UTF bytes in their PAX headers.
+- Fix a range error for invalid zero-length PAX entries.
+
 ## 1.0.2
 
 - Fix a few typos in documentation comments.
diff --git a/lib/src/reader.dart b/lib/src/reader.dart
index 3148a65..d7cc33c 100644
--- a/lib/src/reader.dart
+++ b/lib/src/reader.dart
@@ -730,6 +730,18 @@
 
     Never error() => throw TarException.header('Invalid PAX record');
 
+    String decodeString(
+        List<int> data, int start, int end, bool allowMalformed) {
+      assert(start < data.length && end <= data.length && start < end);
+      final decoder = allowMalformed ? unsafeUtf8Decoder : utf8.decoder;
+
+      try {
+        return decoder.convert(data, start, end);
+      } on FormatException {
+        error();
+      }
+    }
+
     while (offset < data.length) {
       // At the start of an entry, expect its length which is terminated by a
       // space char.
@@ -763,13 +775,14 @@
         error();
       }
 
-      // Read the key
+      // Read the key, and ensure there is enough space for the value and the
+      // trailing newline.
       final nextEquals = data.indexOf($equal, offset);
-      if (nextEquals == -1 || nextEquals >= endOfEntry) {
+      if (nextEquals == -1 || nextEquals >= endOfEntry - 1) {
         error();
       }
 
-      final key = utf8.decoder.convert(data, offset, nextEquals);
+      final key = decodeString(data, offset, nextEquals, false);
       // Skip over the equals sign
       offset = nextEquals + 1;
 
@@ -783,7 +796,7 @@
       // If we're seeing weird PAX Version 0.0 sparse keys, expect alternating
       // GNU.sparse.offset and GNU.sparse.numbytes headers.
       if (key == paxGNUSparseNumBytes || key == paxGNUSparseOffset) {
-        final value = utf8.decoder.convert(data, offset, endOfValue);
+        final value = decodeString(data, offset, endOfValue, false);
 
         if (!_isValidPaxRecord(key, value) ||
             (sparseMap.length.isEven && key != paxGNUSparseOffset) ||
@@ -796,7 +809,7 @@
       } else if (!ignoreUnknown || supportedPaxHeaders.contains(key)) {
         // Ignore unrecognized headers to avoid unbounded growth of the global
         // header map.
-        final value = unsafeUtf8Decoder.convert(data, offset, endOfValue);
+        final value = decodeString(data, offset, endOfValue, true);
 
         if (!_isValidPaxRecord(key, value)) {
           error();
diff --git a/pubspec.yaml b/pubspec.yaml
index bed57e7..be54f62 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: tar
 description: Memory-efficient, streaming implementation of the tar file format
-version: 1.0.2
+version: 1.0.3
 repository: https://github.com/simolus3/tar/
 
 environment:
diff --git a/reference/bad/invalid_pax_header.tar b/reference/bad/invalid_pax_header.tar
new file mode 100644
index 0000000..6a5c581
--- /dev/null
+++ b/reference/bad/invalid_pax_header.tar
Binary files differ
diff --git a/reference/bad/invalid_pax_len.tar b/reference/bad/invalid_pax_len.tar
new file mode 100644
index 0000000..ba49b86
--- /dev/null
+++ b/reference/bad/invalid_pax_len.tar
Binary files differ
diff --git a/test/reader_test.dart b/test/reader_test.dart
index d1abde5..f0a2147 100644
--- a/test/reader_test.dart
+++ b/test/reader_test.dart
@@ -972,6 +972,18 @@
     });
   });
 
+  test('throws for invalid utf8 in pax key', () async {
+    final reader =
+        TarReader(fs.file('reference/bad/invalid_pax_header.tar').openRead());
+    expect(reader.moveNext(), throwsA(isA<TarException>()));
+  });
+
+  test('throws for zero-length pax data', () async {
+    final reader =
+        TarReader(fs.file('reference/bad/invalid_pax_len.tar').openRead());
+    expect(reader.moveNext(), throwsA(isA<TarException>()));
+  });
+
   group('PAX headers', () {
     test('locals overwrite globals', () {
       final header = PaxHeaders()
diff --git a/tool/fuzz.dart b/tool/fuzz.dart
new file mode 100644
index 0000000..0909385
--- /dev/null
+++ b/tool/fuzz.dart
@@ -0,0 +1,30 @@
+import 'dart:io';
+
+import 'package:tar/tar.dart';
+
+const verbose = bool.fromEnvironment('verbose');
+
+/// Reads tar files from arguments and expects not to crash.
+///
+/// When something goes wrong, the name of the problematic tar file is printed.
+/// By running with `-Dverbose=true`, a stack trace is printed as well.
+void main(List<String> files) async {
+  for (final file in files) {
+    try {
+      await TarReader.forEach(File(file).openRead(), (entry) {});
+    } on TarException {
+      // These are fine
+    } on Object catch (e, s) {
+      // Other exceptions indicate a bug in pkg:tar
+      if (verbose) {
+        print(e);
+        print(s);
+      } else {
+        print(
+            'failed for $file - run `dart -Dverbose=true tool/fuzz.dart $file`');
+      }
+
+      exit(128);
+    }
+  }
+}
diff --git a/tool/run_fuzz.sh b/tool/run_fuzz.sh
new file mode 100755
index 0000000..0ad165f
--- /dev/null
+++ b/tool/run_fuzz.sh
@@ -0,0 +1,9 @@
+rm -r /tmp/fuzz
+mkdir /tmp/fuzz
+dart compile exe tool/fuzz.dart -o /tmp/fuzz/fuzz.exe
+
+while true; do
+   radamsa -o /tmp/fuzz/gen-%n -n 100 reference/**/*.tar
+   /tmp/fuzz/fuzz.exe /tmp/fuzz/gen-*
+   test $? -gt 127 && break
+done