Utf8.fromUtf8(): allow passing the length (#61)

The string could be non-zero-terminated, for example, or even if it is,
the length may be already known, in which case an unnecessary strlen()
call can be avoided.
diff --git a/lib/src/utf8.dart b/lib/src/utf8.dart
index 38476c7..689646f 100644
--- a/lib/src/utf8.dart
+++ b/lib/src/utf8.dart
@@ -31,15 +31,20 @@
 
   /// Creates a [String] containing the characters UTF-8 encoded in [string].
   ///
-  /// The [string] must be a zero-terminated byte sequence of valid UTF-8
-  /// encodings of Unicode scalar values. A [FormatException] is thrown if the
-  /// input is malformed. See [Utf8Decoder] for details on decoding.
+  /// Either the [string] must be zero-terminated or its [length] — the
+  /// number of bytes — must be specified as a non-negative value. The
+  /// byte sequence must be valid UTF-8 encodings of Unicode scalar values. A
+  /// [FormatException] is thrown if the input is malformed. See [Utf8Decoder]
+  /// for details on decoding.
   ///
   /// Returns a Dart string containing the decoded code points.
-  static String fromUtf8(Pointer<Utf8> string) {
-    final int length = strlen(string);
-    return utf8.decode(Uint8List.view(
-        string.cast<Uint8>().asTypedList(length).buffer, 0, length));
+  static String fromUtf8(Pointer<Utf8> string, {int? length}) {
+    if (length != null) {
+      RangeError.checkNotNegative(length, 'length');
+    } else {
+      length = strlen(string);
+    }
+    return utf8.decode(string.cast<Uint8>().asTypedList(length));
   }
 
   /// Convert a [String] to a UTF-8 encoded zero-terminated C string.
diff --git a/test/utf8_test.dart b/test/utf8_test.dart
index 61f3b07..d8d91ec 100644
--- a/test/utf8_test.dart
+++ b/test/utf8_test.dart
@@ -55,4 +55,38 @@
     final Pointer<Utf8> utf8 = _bytesFromList([0x80, 0x00]).cast();
     expect(() => Utf8.fromUtf8(utf8), throwsA(isFormatException));
   });
+
+  test('fromUtf8 ASCII with length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]).cast();
+    final String end = Utf8.fromUtf8(utf8, length: 5);
+    expect(end, 'Hello');
+  });
+
+  test('fromUtf8 emoji with length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [240, 159, 152, 142, 240, 159, 145, 191, 240, 159, 146, 172, 0]).cast();
+    final String end = Utf8.fromUtf8(utf8, length: 4);
+    expect(end, '😎');
+  });
+
+  test('fromUtf8 with zero length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]).cast();
+    final String end = Utf8.fromUtf8(utf8, length: 0);
+    expect(end, '');
+  });
+
+  test('fromUtf8 with negative length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]).cast();
+    expect(() => Utf8.fromUtf8(utf8, length: -1), throwsRangeError);
+  });
+
+  test('fromUtf8 with length and containing a zero byte', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 0, 87, 111, 114, 108, 100, 33, 10]).cast();
+    final String end = Utf8.fromUtf8(utf8, length: 13);
+    expect(end, 'Hello\x00World!\n');
+  });
 }