OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 part of dart.convert; | |
6 | |
7 /** | |
8 * An instance of [Base64Codec]. | |
9 * | |
10 * This instance provides a convenient access to the most common | |
11 * [BASE64](https://tools.ietf.org/html/rfc4648) use cases. | |
12 * | |
13 * It encodes and decodes using the default alphabet and does not allow | |
14 * any invalid characters in the input to decoding. | |
sra1
2015/09/28 17:29:42
Is this encoding/decoding compatible with btoa()/a
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Compatible, but not identical.
The atob conversio
| |
15 * | |
16 * Examples: | |
17 * | |
18 * var encoded = BASE64.encode([0x62, 0x6c, 0xc3, 0xa5, 0x62, 0xc3, 0xa6, | |
19 * 0x72, 0x67, 0x72, 0xc3, 0xb8, 0x64]); | |
20 * var decoded = BASE64.decode("YmzDpWLDpnJncsO4ZAo="); | |
21 */ | |
22 const Base64Codec BASE64 = const Base64Codec(); | |
Lasse Reichstein Nielsen
2015/09/29 10:31:04
This naming is *only* for consistency. I'd prefer
| |
23 | |
24 /** | |
25 * The default encoding alphabet. | |
Søren Gjesse
2015/09/28 17:18:52
This is not just "the default encoding alphabet" i
Lasse Reichstein Nielsen
2015/09/29 10:31:05
Rewording.
It is the default alphabet for base64 e
| |
26 */ | |
27 const String _base64Alphabet = | |
28 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | |
29 | |
30 const int _paddingChar = 0x3d; // '='. | |
31 const int _sixBitMask = 0x3F; | |
32 const int _eightBitMask = 0xFF; | |
33 | |
34 | |
35 /** | |
36 * A [Base64Codec] allows encoding bytes as BASE64 strings | |
37 * and decoding BASE64 string to bytes. | |
38 */ | |
39 class Base64Codec extends Codec<List<int>, String> { | |
40 const Base64Codec(); | |
41 | |
42 String get name => "base64"; | |
Søren Gjesse
2015/09/28 17:18:52
Uppercase B?
Lasse Reichstein Nielsen
2015/09/29 10:31:05
I should remove it. This is not an "Encoding", jus
| |
43 | |
44 Base64Encoder get encoder => const Base64Encoder(); | |
45 | |
46 Base64Decoder get decoder => const Base64Decoder(); | |
47 } | |
48 | |
49 // ------------------------------------------------------------------------ | |
50 // Encoder | |
51 // ------------------------------------------------------------------------ | |
52 | |
53 class Base64Encoder extends Converter<List<int>, String> { | |
54 const Base64Encoder(); | |
55 | |
56 String convert(List<int> input) { | |
57 if (input.isEmpty) return ""; | |
58 var encoder = new _Base64Encoder(); | |
floitsch
2015/09/28 16:08:28
Up to you if you want to add a type here.
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Not really worth it.
| |
59 Uint8List buffer = encoder._encode(input, 0, input.length, true); | |
60 return new String.fromCharCodes(buffer); | |
61 } | |
62 | |
63 /** | |
64 * Starts a chunked conversion. | |
65 */ | |
66 ByteConversionSink startChunkedConversion(Sink<String> sink) { | |
67 if (sink is StringConversionSink) { | |
68 return new _Utf8Base64EncoderSink(sink.asUtf8Sink()); | |
69 } | |
70 return new _AsciiBase64EncoderSink(sink); | |
71 } | |
72 } | |
73 | |
74 /** | |
75 * Helper class for encoding bytes to BASE-64. | |
floitsch
2015/09/28 16:08:28
usage is not consistent. Sometimes BASE64. Here BA
Lasse Reichstein Nielsen
2015/09/29 10:31:04
I think the name should be "base64" or "base 64 en
| |
76 */ | |
77 class _Base64Encoder { | |
floitsch
2015/09/28 16:08:28
I'm not a fan of reusing a class as a mixin.
I wo
Lasse Reichstein Nielsen
2015/09/29 10:31:05
That would be pretty redundant for a private helpe
| |
78 /** Intermediiate state shift of the bits stored in the state. */ | |
floitsch
2015/09/28 16:08:28
Intermediate
/// Shift-count to extract the value
Lasse Reichstein Nielsen
2015/09/29 10:31:05
Done.
| |
79 static const int _valueShift = 2; | |
80 /** Intermediate state encoding of the number of bytes stored in the state. */ | |
floitsch
2015/09/28 16:08:28
/// Mask to extract the XYZ count from the state.
Lasse Reichstein Nielsen
2015/09/29 10:31:05
Done.
| |
81 static const int _countMask = 3; | |
82 | |
83 int _state = 0; | |
floitsch
2015/09/28 16:08:28
Might be nice to just write getters and setters.
Lasse Reichstein Nielsen
2015/09/29 10:31:05
I use the extraction in static methods, so I added
| |
84 | |
85 Uint8List _getBuffer(int bufferLength) => new Uint8List(bufferLength); | |
floitsch
2015/09/28 16:08:28
_createBuffer
I do realize that "_buffer()" in my
Lasse Reichstein Nielsen
2015/09/29 10:31:05
Yes, "buffer" as a verb is not going to work, it's
| |
86 | |
87 Uint8List _encode(List<int> bytes, int start, int end, bool isLast) { | |
88 assert(0 <= start); | |
89 assert(start <= end); | |
90 assert(bytes == null || end <= bytes.length); | |
91 int length = end - start; | |
92 | |
93 int count = _state & _countMask; | |
94 int byteCount = (count + length); | |
95 int fullChunks = byteCount ~/ 3; | |
96 int partialChunkLength = byteCount - fullChunks * 3; | |
97 int bufferLength = fullChunks * 4; | |
98 if (isLast && partialChunkLength > 0) { | |
99 bufferLength += 4; // Room for padding. | |
100 } | |
101 var output = _getBuffer(bufferLength); | |
floitsch
2015/09/28 16:08:28
type if you want to.
| |
102 _state = _encodeChunk(bytes, start, end, isLast, output, 0, _state); | |
103 if (bufferLength > 0) return output; | |
104 // If the input plus the data in _state is still less than three bytes, | |
floitsch
2015/09/28 16:08:28
-still-
Lasse Reichstein Nielsen
2015/09/29 10:31:05
Done.
| |
105 // there may not be any output. | |
floitsch
2015/09/28 16:08:28
s/may not be any/is no
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Unless isLast is true, in which case there is outp
| |
106 return null; | |
107 } | |
108 | |
109 static int _encodeChunk(List<int> bytes, int start, int end, bool isLast, | |
110 Uint8List output, int outputIndex, int state) { | |
111 int bits = state >> _valueShift; | |
112 // Count number of missing bytes in three-byte chunk. | |
113 int count = 3 - (state & _countMask); | |
114 | |
115 int byteOr = 0; | |
floitsch
2015/09/28 16:08:28
// The input must be a list of bytes.
// All input
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Done.
| |
116 for (int i = start; i < end; i++) { | |
117 int byte = bytes[i]; | |
118 byteOr |= byte; | |
119 bits = (bits << 8) | byte; | |
120 count--; | |
floitsch
2015/09/28 16:08:28
I find "count" to be misleading (but it's not too
Lasse Reichstein Nielsen
2015/09/29 10:31:05
Renamed to "expectedChars".
| |
121 if (count == 0) { | |
122 output[outputIndex++] = | |
123 _base64Alphabet.codeUnitAt((bits >> 18) & _sixBitMask); | |
124 output[outputIndex++] = | |
125 _base64Alphabet.codeUnitAt((bits >> 12) & _sixBitMask); | |
126 output[outputIndex++] = | |
127 _base64Alphabet.codeUnitAt((bits >> 6) & _sixBitMask); | |
128 output[outputIndex++] = | |
129 _base64Alphabet.codeUnitAt(bits & _sixBitMask); | |
130 count = 3; | |
131 bits = 0; | |
132 } | |
133 } | |
134 if (byteOr >= 0 && byteOr <= 255) { | |
floitsch
2015/09/28 16:08:28
I almost prefer to have the error-case guarded:
i
Lasse Reichstein Nielsen
2015/09/29 10:31:04
I like to keep the non-error flow connected if pos
| |
135 if (isLast && count < 3) { | |
136 _writeFinalChunk(output, outputIndex, 3 - count, bits); | |
137 return 0; | |
138 } | |
139 return (bits << _valueShift) | (3 - count); | |
140 } | |
141 | |
142 // There was an invalid byte value somewhere in the input - find it! | |
143 int i = start; | |
144 while (i < end) { | |
145 int byte = bytes[i]; | |
146 if (byte < 0 || byte > 255) break; | |
147 i++; | |
148 } | |
149 throw new ArgumentError.value(bytes, | |
150 "Not a byte value at index $i: 0x${bytes[i].toRadixString(16)}"); | |
151 } | |
152 | |
153 /** | |
154 * Writes a final encoded four-character chunk. | |
155 * | |
156 * Only used when the [state] contains a partial (1 or 2 byte) | |
157 * input. | |
158 */ | |
159 static void _writeFinalChunk(Uint8List output, int outputIndex, | |
160 int count, int bits) { | |
161 assert(count > 0); | |
162 if (count == 1) { | |
Søren Gjesse
2015/09/28 17:18:52
Maybe this could be
if (count == 2) {
outpu
Lasse Reichstein Nielsen
2015/09/29 10:31:04
It's >> 2, << 4 for one of the branches, so that d
| |
163 output[outputIndex++] = | |
164 _base64Alphabet.codeUnitAt((bits >> 2) & _sixBitMask); | |
165 output[outputIndex++] = | |
166 _base64Alphabet.codeUnitAt((bits << 4) & _sixBitMask); | |
167 output[outputIndex++] = _paddingChar; | |
168 output[outputIndex++] = _paddingChar; | |
169 } else { | |
170 assert(count == 2); | |
171 output[outputIndex++] = | |
172 _base64Alphabet.codeUnitAt((bits >> 10) & _sixBitMask); | |
173 output[outputIndex++] = | |
174 _base64Alphabet.codeUnitAt((bits >> 4) & _sixBitMask); | |
175 output[outputIndex++] = | |
176 _base64Alphabet.codeUnitAt((bits << 2) & _sixBitMask); | |
177 output[outputIndex++] = _paddingChar; | |
178 } | |
179 } | |
180 } | |
181 | |
182 abstract class _Base64EncoderSink extends ByteConversionSinkBase | |
183 with _Base64Encoder { | |
184 void add(List<int> source) { | |
185 _add(source, 0, source.length, false); | |
186 } | |
187 | |
188 void close() { | |
189 _add(null, 0, 0, true); | |
190 } | |
191 | |
192 void addSlice(List<int> source, int start, int end, bool isLast) { | |
193 if (end == null) throw new ArgumentError.notNull("end"); | |
194 RangeError.checkValidRange(start, end, source.length); | |
195 _add(source, start, end, isLast); | |
196 } | |
197 | |
198 void _add(List<int> source, int start, int end, bool isLast); | |
199 } | |
200 | |
201 class _AsciiBase64EncoderSink extends _Base64EncoderSink { | |
202 final ChunkedConversionSink<String> _sink; | |
203 /** | |
Søren Gjesse
2015/09/28 17:18:52
Shouldn't be dartdoc.
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Why not?
An editor should still show it to you, ev
| |
204 * Reused buffer. | |
205 * | |
206 * Since the buffer isn't released to the sink, only the string created | |
207 * from it, the buffer can be reused between chunks. | |
208 */ | |
209 Uint8List _bufferCache; | |
210 | |
211 _AsciiBase64EncoderSink(this._sink); | |
212 | |
213 Uint8List _getBuffer(int bufferLength) { | |
214 if (_bufferCache == null || _bufferCache.length < bufferLength) { | |
215 _bufferCache = new Uint8List(bufferLength); | |
216 } | |
217 return new Uint8List.view(_bufferCache.buffer, 0, bufferLength); | |
218 } | |
219 | |
220 void _add(List<int> source, int start, int end, bool isLast) { | |
221 Uint8List buffer = _encode(source, start, end, isLast); | |
222 if (buffer != null) { | |
223 String string = new String.fromCharCodes(buffer); | |
224 _sink.add(string); | |
225 } | |
226 if (isLast) { | |
227 _sink.close(); | |
228 } | |
229 } | |
230 } | |
231 | |
232 class _Utf8Base64EncoderSink extends _Base64EncoderSink { | |
233 final ByteConversionSink _sink; | |
234 _Utf8Base64EncoderSink(this._sink, [int bufferSize]) : super(bufferSize); | |
235 | |
236 void _add(List<int> source, int start, int end, bool isLast) { | |
237 Uint8List buffer = _encode(source, start, end, isLast); | |
238 if (buffer != null) { | |
239 _sink.addSlice(buffer, 0, buffer.length, isLast); | |
240 } | |
241 } | |
242 } | |
243 | |
244 // ------------------------------------------------------------------------ | |
245 // Decoder | |
246 // ------------------------------------------------------------------------ | |
247 | |
248 class Base64Decoder extends Converter<String, List<int>> { | |
249 const Base64Decoder(); | |
250 | |
251 List<int> convert(String input) { | |
252 if (input.isEmpty) return new Uint8List(0); | |
253 int length = input.length; | |
254 if (length % 4 != 0) { | |
255 throw new FormatException("Invalid length, must be multiple of four", | |
256 input, length); | |
257 } | |
258 var decoder = new _Base64Decoder(); | |
floitsch
2015/09/28 16:08:28
type if you want.
| |
259 Uint8List buffer = decoder._decode(input, 0, input.length); | |
260 decoder._close(input, input.length); | |
261 return buffer; | |
sra1
2015/09/28 17:29:42
If you are OK with an unmodifiable result, the a b
Lasse Reichstein Nielsen
2015/09/29 10:31:05
I probably want it to be a Uint8List.
Still, it sh
| |
262 } | |
263 | |
264 StringConversionSink startChunkedConversion(Sink<List<int>> sink) { | |
265 return new _Base64DecoderSink(sink); | |
266 } | |
267 } | |
268 | |
269 /** | |
270 * Helper class implementing BASE64 decoding with intermediate state. | |
271 */ | |
272 class _Base64Decoder { | |
273 static const int _valueShift = 2; | |
floitsch
2015/09/28 16:08:28
Same as for the encoder.
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Done.
| |
274 static const int _countMask = 3; | |
275 | |
276 /** Invalid character in decoding table. */ | |
277 static const int _invalid = -2; | |
278 /** Padding character in decoding table. */ | |
279 static const int _padding = -1; | |
280 | |
281 // Shorthand to make the table more readable. | |
282 static const int __ = _invalid; | |
283 static const int _p = _padding; | |
284 | |
285 /** | |
286 * Mapping from ASCII characters to their index in [_base64alphabet]. | |
287 * | |
288 * Uses -1 for invalid indices and 64 for the padding character. | |
289 */ | |
290 static final List<int> _inverseAlphabet = new Int8List.fromList([ | |
291 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, | |
292 __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, | |
293 __, __, __, __, __, __, __, __, __, __, __, 62, __, __, __, 63, | |
294 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, __, __, __, _p, __, __, | |
295 __, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, | |
296 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, __, __, __, __, __, | |
297 __, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, | |
298 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, __, __, __, __, __, | |
299 ]); | |
300 | |
301 /** | |
302 * Maintains the intermediate state of a partly-decoded input. | |
303 * | |
304 * BASE-64 is decoded in chunks of four characters. If a chunk does not | |
305 * contain a full block, the decoded bits (six per character) of the | |
306 * available characters are stored in [_state] until the next call to | |
307 * [_decode] or [_close]. | |
308 * | |
309 * If no padding has been seen, the value is | |
310 * `numberOfCharactersSeen | (decodedBits << 2)` | |
311 * where `numberOfCharactersSeen` is between 0 and 3 and decoded bits | |
312 * contains six bits per seend character. | |
floitsch
2015/09/28 16:08:28
seen
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Done.
| |
313 * | |
314 * If padding has been seen the value is negative. It's the bitwise negation | |
315 * of the number of remanining allowed padding characters (always ~0 or ~1). | |
316 * | |
317 * A state of `0` or `~0` are valid places to end decoding, all other values | |
318 * means that a four-character block has not been completed. | |
floitsch
2015/09/28 16:08:28
mean
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Done.
| |
319 */ | |
320 int _state = 0; | |
321 | |
322 /** | |
323 * Decodes [input] from [start] to [end]. | |
324 * | |
325 * Returns a [Uint8List] with the decoded bytes. | |
326 * If a previous call had an incomplete four-character block, the bits from | |
327 * those are included in decoding | |
328 */ | |
329 Uint8List _decode(String input, int start, int end) { | |
330 assert(0 <= start); | |
331 assert(start <= end); | |
332 assert(end <= input.length); | |
333 if (_state < 0) { | |
334 _state = _checkPadding(input, start, end, _state); | |
335 return null; | |
336 } | |
337 if (start == end) return new Uint8List(0); | |
338 Uint8List buffer = _allocateBuffer(input, start, end, _state); | |
339 _state = _decodeChunk(input, start, end, buffer, 0, _state); | |
340 return buffer; | |
341 } | |
342 | |
343 /** Checks that [state] represents a valid decoding. */ | |
344 void _close(String input, int end) { | |
345 if (_state < ~0) { | |
346 throw new FormatException("Missing padding character", input, end); | |
347 } | |
348 if (_state > 0) { | |
349 throw new FormatException("Invalid length, must be multiple of four", | |
350 input, end); | |
351 } | |
352 _state = ~0; | |
353 } | |
354 | |
355 /** | |
356 * Decodes [input] from [start] to [end]. | |
357 * | |
358 * Includes the state returned by a previous call in the decoding. | |
359 * Writes the decoding to [output] at [outIndex], and there must | |
360 * be room in the output. | |
361 */ | |
362 static int _decodeChunk(String input, int start, int end, | |
363 Uint8List output, int outIndex, | |
364 int state) { | |
365 const int asciiMask = 127; | |
366 const int asciiMax = 127; | |
367 int bits = state >> _valueShift; | |
368 int count = state & _countMask; | |
369 int charOr = 0; | |
floitsch
2015/09/28 16:08:28
Add comment, what the charOr is for.
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Done.
| |
370 for (int i = start; i < end; i++) { | |
371 var char = input.codeUnitAt(i); | |
floitsch
2015/09/28 16:08:28
type if you want.
| |
372 charOr |= char; | |
373 int code = _inverseAlphabet[char & asciiMask]; | |
374 if (code >= 0) { | |
375 bits = ((bits << 6) | code); | |
floitsch
2015/09/28 16:08:28
Add & to make it easier for the VM to optimize.
floitsch
2015/09/28 16:08:28
magic "6".
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Done.
Lasse Reichstein Nielsen
2015/09/29 10:31:04
named it "bitsPerCharacter".
| |
376 count = (count + 1) & 3; | |
377 if (count == 0) { | |
378 assert(outIndex + 3 <= output.length); | |
379 output[outIndex++] = (bits >> 16) & _eightBitMask; | |
380 output[outIndex++] = (bits >> 8) & _eightBitMask; | |
381 output[outIndex++] = bits & _eightBitMask; | |
382 bits = 0; | |
383 } | |
384 continue; | |
385 } else if (code == _padding && count > 1) { | |
386 if (count == 3) { | |
387 if ((bits & 0x03) != 0) { | |
388 throw new FormatException( | |
389 "Invalid encoding before padding", input, i); | |
390 } | |
391 output[outIndex++] = bits >> 10; | |
392 output[outIndex++] = bits >> 2; | |
393 } else { | |
394 if ((bits & 0x0F) != 0) { | |
395 throw new FormatException( | |
396 "Invalid encoding before padding", input, i); | |
397 } | |
398 output[outIndex++] = bits >> 4; | |
399 } | |
400 int expectedPadding = 3 - count; | |
401 state = _checkPadding(input, i + 1, end, ~expectedPadding); | |
402 return state; | |
403 } | |
404 throw new FormatException("Invalid character", input, i); | |
405 } | |
406 if (charOr >= 0 && charOr <= asciiMax) { | |
floitsch
2015/09/28 16:08:28
Same as for the encoder.
Lasse Reichstein Nielsen
2015/09/29 10:31:04
Same answer.
| |
407 return (bits << _valueShift) | count; | |
408 } | |
409 // There is an invalid (non-ASCII) character in the input. | |
410 int i; | |
411 for (i = start; i < end; i++) { | |
412 int char = input.codeUnitAt(i); | |
413 if (char < 0 || char > asciiMax) break; | |
414 } | |
415 throw new FormatException("Invalid character", input, i); | |
416 } | |
417 | |
418 /** | |
419 * Allocates a buffer with room for the decoding of a substring of [input]. | |
420 * | |
421 * Includes room for the characters in [state], and handles padding correctly. | |
422 */ | |
423 static Uint8List _allocateBuffer(String input, int start, int end, | |
424 int state) { | |
425 assert(state >= 0); | |
426 int padding = 0; | |
427 int length = (state & _countMask) + (end - start); | |
428 if (end > start && input.codeUnitAt(end - 1) == _paddingChar) { | |
429 padding++; | |
430 if (end - 1 > start && input.codeUnitAt(end - 2) == _paddingChar) { | |
431 padding++; | |
432 } | |
433 } | |
434 // Three bytes per full four bytes in the input. | |
435 int bufferLength = (length >> 2) * 3; | |
436 // If padding was seen, then remove the padding if it was counter, or | |
437 // add the last partial chunk it it wasn't counted. | |
438 int remainderLength = length & 3; | |
439 if (remainderLength == 0) { | |
440 bufferLength -= padding; | |
441 } else if (padding != 0 && remainderLength - padding > 1) { | |
442 bufferLength += remainderLength - 1 - padding; | |
443 } | |
444 if (bufferLength > 0) return new Uint8List(bufferLength); | |
445 // If the input plus state is still less than four characters, no buffer | |
446 // is needed. | |
447 return null; | |
448 } | |
449 | |
450 /** | |
451 * Check that the remainder of the string is valid padding. | |
452 * | |
453 * That means zero or one padding character (depending on [_state]) | |
454 * and nothing else. | |
455 */ | |
456 static int _checkPadding(String input, int start, int end, int state) { | |
457 assert(state < 0); | |
458 if (start == end) return state; | |
459 int expectedPadding = ~state; | |
460 if (expectedPadding > 0) { | |
461 int firstChar = input.codeUnitAt(start); | |
462 if (firstChar != _paddingChar) { | |
463 throw new FormatException("Missing padding character", string, start); | |
464 } | |
465 state = ~0; | |
466 start++; | |
467 } | |
468 if (start != end) { | |
469 throw new FormatException("Invalid character after padding", | |
470 input, start); | |
471 } | |
472 return state; | |
473 } | |
474 } | |
475 | |
476 class _Base64DecoderSink extends StringConversionSinkBase with _Base64Decoder { | |
477 /** Output sink */ | |
478 final ChunkedConversionSink<List<int>> _sink; | |
479 | |
480 _Base64DecoderSink(this._sink); | |
481 | |
482 void add(String string) { | |
483 if (string.isEmpty) return; | |
484 Uint8List buffer = _decode(string, 0, string.length); | |
485 if (buffer != null) _sink.add(buffer); | |
486 } | |
487 | |
488 void close() { | |
489 _close(null, null); | |
490 _sink.close(); | |
491 } | |
492 | |
493 void addSlice(String string, int start, int end, bool isLast) { | |
494 end = RangeError.checkValidRange(start, end, string.length); | |
495 if (start == end) return; | |
496 Uint8List buffer = _decode(string, start, end); | |
497 if (buffer != null) _sink.add(buffer); | |
498 if (isLast) { | |
499 _close(string, end); | |
500 _sink.close(); | |
501 } | |
502 } | |
503 } | |
OLD | NEW |