Fix chunkedCoding. (#9)
I misread the spec; it turns out there's a CR LF sequence after the body
bytes as well as after the size header.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c12d2cb..2c50995 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 3.1.1
+
+* Fix a logic bug in the `chunkedCoding` codec. It had been producing invalid
+ output and rejecting valid input.
+
## 3.1.0
* Add `chunkedCoding`, a `Codec` that supports encoding and decoding the
diff --git a/lib/src/chunked_coding/decoder.dart b/lib/src/chunked_coding/decoder.dart
index e2a27fc..18530bc 100644
--- a/lib/src/chunked_coding/decoder.dart
+++ b/lib/src/chunked_coding/decoder.dart
@@ -70,7 +70,7 @@
/// describe the character in the exception text.
assertCurrentChar(int char, String name) {
if (bytes[start] != char) {
- throw new FormatException("Expected LF.", bytes, start);
+ throw new FormatException("Expected $name.", bytes, start);
}
}
@@ -85,7 +85,7 @@
case _State.size:
if (bytes[start] == $cr) {
- _state = _State.beforeLF;
+ _state = _State.sizeBeforeLF;
} else {
// Shift four bits left since a single hex digit contains four bits
// of information.
@@ -94,7 +94,7 @@
start++;
break;
- case _State.beforeLF:
+ case _State.sizeBeforeLF:
assertCurrentChar($lf, "LF");
_state = _size == 0 ? _State.endBeforeCR : _State.body;
start++;
@@ -105,7 +105,19 @@
buffer.addAll(bytes, start, chunkEnd);
_size -= chunkEnd - start;
start = chunkEnd;
- if (_size == 0) _state = _State.boundary;
+ if (_size == 0) _state = _State.bodyBeforeCR;
+ break;
+
+ case _State.bodyBeforeCR:
+ assertCurrentChar($cr, "CR");
+ _state = _State.bodyBeforeLF;
+ start++;
+ break;
+
+ case _State.bodyBeforeLF:
+ assertCurrentChar($lf, "LF");
+ _state = _State.boundary;
+ start++;
break;
case _State.endBeforeCR:
@@ -115,7 +127,7 @@
break;
case _State.endBeforeLF:
- assertCurrentChar($lf, "CR");
+ assertCurrentChar($lf, "LF");
_state = _State.end;
start++;
break;
@@ -161,8 +173,6 @@
/// An enumeration of states that [_Sink] can exist in when decoded a chunked
/// message.
-///
-/// [_SizeState], [_CRState], and [_ChunkState] have additional data attached.
class _State {
/// The parser has fully parsed one chunk and is expecting the header for the
/// next chunk.
@@ -173,20 +183,32 @@
/// The parser has parsed at least one digit of the chunk size header, but has
/// not yet parsed the `CR LF` sequence that indicates the end of that header.
///
- /// Transitions to [beforeLF].
+ /// Transitions to [sizeBeforeLF].
static const size = const _State._("size");
/// The parser has parsed the chunk size header and the CR character after it,
/// but not the LF.
///
- /// Transitions to [body] or [endBeforeCR].
- static const beforeLF = const _State._("before LF");
+ /// Transitions to [body] or [bodyBeforeCR].
+ static const sizeBeforeLF = const _State._("size before LF");
/// The parser has parsed a chunk header and possibly some of the body, but
/// still needs to consume more bytes.
///
- /// Transitions to [boundary].
- static const body = const _State._("CR");
+ /// Transitions to [bodyBeforeCR].
+ static const body = const _State._("body");
+
+ // The parser has parsed all the bytes in a chunk body but not the CR LF
+ // sequence that follows it.
+ //
+ // Transitions to [bodyBeforeLF].
+ static const bodyBeforeCR = const _State._("body before CR");
+
+ // The parser has parsed all the bytes in a chunk body and the CR that follows
+ // it, but not the LF after that.
+ //
+ // Transitions to [bounday].
+ static const bodyBeforeLF = const _State._("body before LF");
/// The parser has parsed the final empty chunk but not the CR LF sequence
/// that follows it.
diff --git a/lib/src/chunked_coding/encoder.dart b/lib/src/chunked_coding/encoder.dart
index c724700..183ecfc 100644
--- a/lib/src/chunked_coding/encoder.dart
+++ b/lib/src/chunked_coding/encoder.dart
@@ -59,12 +59,18 @@
var sizeInHex = size.toRadixString(16);
var footerSize = isLast ? _doneChunk.length : 0;
- // Add 2 for the CRLF sequence that follows the size header.
- var list = new Uint8List(sizeInHex.length + 2 + size + footerSize);
+ // Add 4 for the CRLF sequences that follow the size header and the bytes.
+ var list = new Uint8List(sizeInHex.length + 4 + size + footerSize);
list.setRange(0, sizeInHex.length, sizeInHex.codeUnits);
- list[sizeInHex.length] = $cr;
- list[sizeInHex.length + 1] = $lf;
- list.setRange(sizeInHex.length + 2, list.length - footerSize, bytes, start);
+
+ var cursor = sizeInHex.length;
+ list[cursor++] = $cr;
+ list[cursor++] = $lf;
+ list.setRange(cursor, cursor + end - start, bytes, start);
+ cursor += end - start;
+ list[cursor++] = $cr;
+ list[cursor++] = $lf;
+
if (isLast) {
list.setRange(list.length - footerSize, list.length, _doneChunk);
}
diff --git a/test/chunked_coding_test.dart b/test/chunked_coding_test.dart
index 29fce25..241cfc9 100644
--- a/test/chunked_coding_test.dart
+++ b/test/chunked_coding_test.dart
@@ -14,7 +14,7 @@
group("encoder", () {
test("adds a header to the chunk of bytes", () {
expect(chunkedCoding.encode([1, 2, 3]),
- equals([$3, $cr, $lf, 1, 2, 3, $0, $cr, $lf, $cr, $lf]));
+ equals([$3, $cr, $lf, 1, 2, 3, $cr, $lf, $0, $cr, $lf, $cr, $lf]));
});
test("uses hex for chunk size", () {
@@ -22,7 +22,7 @@
expect(chunkedCoding.encode(data),
equals([$a, $7, $cr, $lf]
..addAll(data)
- ..addAll([$0, $cr, $lf, $cr, $lf])));
+ ..addAll([$cr, $lf, $0, $cr, $lf, $cr, $lf])));
});
test("just generates a footer for an empty input", () {
@@ -41,18 +41,18 @@
test("adds headers to each chunk of bytes", () {
sink.add([1, 2, 3, 4]);
- expect(results, equals([[$4, $cr, $lf, 1, 2, 3, 4]]));
+ expect(results, equals([[$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf]]));
sink.add([5, 6, 7]);
expect(results, equals([
- [$4, $cr, $lf, 1, 2, 3, 4],
- [$3, $cr, $lf, 5, 6, 7],
+ [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf],
+ [$3, $cr, $lf, 5, 6, 7, $cr, $lf],
]));
sink.close();
expect(results, equals([
- [$4, $cr, $lf, 1, 2, 3, 4],
- [$3, $cr, $lf, 5, 6, 7],
+ [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf],
+ [$3, $cr, $lf, 5, 6, 7, $cr, $lf],
[$0, $cr, $lf, $cr, $lf],
]));
});
@@ -62,15 +62,15 @@
expect(results, equals([[]]));
sink.add([1, 2, 3]);
- expect(results, equals([[], [$3, $cr, $lf, 1, 2, 3]]));
+ expect(results, equals([[], [$3, $cr, $lf, 1, 2, 3, $cr, $lf]]));
sink.add([]);
- expect(results, equals([[], [$3, $cr, $lf, 1, 2, 3], []]));
+ expect(results, equals([[], [$3, $cr, $lf, 1, 2, 3, $cr, $lf], []]));
sink.close();
expect(results, equals([
[],
- [$3, $cr, $lf, 1, 2, 3],
+ [$3, $cr, $lf, 1, 2, 3, $cr, $lf],
[],
[$0, $cr, $lf, $cr, $lf],
]));
@@ -79,7 +79,7 @@
group("addSlice()", () {
test("adds bytes from the specified slice", () {
sink.addSlice([1, 2, 3, 4, 5], 1, 4, false);
- expect(results, equals([[$3, $cr, $lf, 2, 3, 4]]));
+ expect(results, equals([[$3, $cr, $lf, 2, 3, 4, $cr, $lf]]));
});
test("doesn't add a header if the slice is empty", () {
@@ -89,8 +89,8 @@
test("adds a footer if isLast is true", () {
sink.addSlice([1, 2, 3, 4, 5], 1, 4, true);
- expect(results,
- equals([[$3, $cr, $lf, 2, 3, 4, $0, $cr, $lf, $cr, $lf]]));
+ expect(results, equals(
+ [[$3, $cr, $lf, 2, 3, 4, $cr, $lf, $0, $cr, $lf, $cr, $lf]]));
// Setting isLast shuld close the sink.
expect(() => sink.add([]), throwsStateError);
@@ -119,8 +119,8 @@
group("decoder", () {
test("parses chunked data", () {
expect(chunkedCoding.decode([
- $3, $cr, $lf, 1, 2, 3,
- $4, $cr, $lf, 4, 5, 6, 7,
+ $3, $cr, $lf, 1, 2, 3, $cr, $lf,
+ $4, $cr, $lf, 4, 5, 6, 7, $cr, $lf,
$0, $cr, $lf, $cr, $lf,
]), equals([1, 2, 3, 4, 5, 6, 7]));
});
@@ -130,7 +130,7 @@
expect(
chunkedCoding.decode([$a, $7, $cr, $lf]
..addAll(data)
- ..addAll([$0, $cr, $lf, $cr, $lf])),
+ ..addAll([$cr, $lf, $0, $cr, $lf, $cr, $lf])),
equals(data));
});
@@ -139,7 +139,7 @@
expect(
chunkedCoding.decode([$A, $7, $cr, $lf]
..addAll(data)
- ..addAll([$0, $cr, $lf, $cr, $lf])),
+ ..addAll([$cr, $lf, $0, $cr, $lf, $cr, $lf])),
equals(data));
});
@@ -170,11 +170,21 @@
throwsFormatException);
});
- test("that ends at a chunk boundary", () {
+ test("that ends after a chunk's bytes", () {
expect(() => chunkedCoding.decode([$1, $cr, $lf, 1]),
throwsFormatException);
});
+ test("that ends after a chunk's CR", () {
+ expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr]),
+ throwsFormatException);
+ });
+
+ test("that ends atfter a chunk's LF", () {
+ expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr, $lf]),
+ throwsFormatException);
+ });
+
test("that ends after the empty chunk", () {
expect(() => chunkedCoding.decode([$0, $cr, $lf]),
throwsFormatException);
@@ -208,10 +218,10 @@
});
test("decodes each chunk of bytes", () {
- sink.add([$4, $cr, $lf, 1, 2, 3, 4]);
+ sink.add([$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf]);
expect(results, equals([[1, 2, 3, 4]]));
- sink.add([$3, $cr, $lf, 5, 6, 7]);
+ sink.add([$3, $cr, $lf, 5, 6, 7, $cr, $lf]);
expect(results, equals([[1, 2, 3, 4], [5, 6, 7]]));
sink.add([$0, $cr, $lf, $cr, $lf]);
@@ -223,7 +233,7 @@
sink.add([]);
expect(results, isEmpty);
- sink.add([$3, $cr, $lf, 1, 2, 3]);
+ sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]);
expect(results, equals([[1, 2, 3]]));
sink.add([]);
@@ -281,6 +291,30 @@
expect(results, equals([[1, 2], [3]]));
});
+ test("after all bytes", () {
+ sink.add([$3, $cr, $lf, 1, 2, 3]);
+ expect(results, equals([[1, 2, 3]]));
+
+ sink.add([$cr, $lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]);
+ expect(results, equals([[1, 2, 3], [2, 3, 4]]));
+ });
+
+ test("after a post-chunk CR", () {
+ sink.add([$3, $cr, $lf, 1, 2, 3, $cr]);
+ expect(results, equals([[1, 2, 3]]));
+
+ sink.add([$lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]);
+ expect(results, equals([[1, 2, 3], [2, 3, 4]]));
+ });
+
+ test("after a post-chunk LF", () {
+ sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]);
+ expect(results, equals([[1, 2, 3]]));
+
+ sink.add([$3, $cr, $lf, 2, 3, 4, $cr, $lf]);
+ expect(results, equals([[1, 2, 3], [2, 3, 4]]));
+ });
+
test("after empty chunk size", () {
sink.add([$0]);
expect(results, isEmpty);