| @internal | 
 | import 'package:async/async.dart'; | 
 | import 'package:meta/meta.dart'; | 
 |  | 
 | import 'exception.dart'; | 
 | import 'utils.dart'; | 
 |  | 
 | /// Represents a [length]-sized fragment at [offset] in a file. | 
 | /// | 
 | /// [SparseEntry]s can represent either data or holes, and we can easily | 
 | /// convert between the two if we know the size of the file, all the sparse | 
 | /// data and all the sparse entries combined must give the full size. | 
 | final class SparseEntry { | 
 |   final int offset; | 
 |   final int length; | 
 |  | 
 |   SparseEntry(this.offset, this.length); | 
 |  | 
 |   int get end => offset + length; | 
 |  | 
 |   @override | 
 |   String toString() => 'offset: $offset, length $length'; | 
 |  | 
 |   @override | 
 |   bool operator ==(Object other) { | 
 |     if (other is! SparseEntry) return false; | 
 |  | 
 |     return offset == other.offset && length == other.length; | 
 |   } | 
 |  | 
 |   @override | 
 |   int get hashCode => offset ^ length; | 
 | } | 
 |  | 
 | /// Generates a stream of the sparse file contents of size [size], given | 
 | /// [sparseHoles] and the raw content in [source]. | 
 | @internal | 
 | Stream<List<int>> sparseStream( | 
 |     Stream<List<int>> source, List<SparseEntry> sparseHoles, int size) { | 
 |   if (sparseHoles.isEmpty) { | 
 |     return ChunkedStreamReader(source).readStream(size); | 
 |   } | 
 |  | 
 |   return _sparseStream(source, sparseHoles, size); | 
 | } | 
 |  | 
 | /// Generates a stream of the sparse file contents of size [size], given | 
 | /// [sparseHoles] and the raw content in [source]. | 
 | /// | 
 | /// [sparseHoles] has to be non-empty. | 
 | Stream<List<int>> _sparseStream( | 
 |     Stream<List<int>> source, List<SparseEntry> sparseHoles, int size) async* { | 
 |   // Current logical position in sparse file. | 
 |   var position = 0; | 
 |  | 
 |   // Index of the next sparse hole in [sparseHoles] to be processed. | 
 |   var sparseHoleIndex = 0; | 
 |  | 
 |   // Iterator through [source] to obtain the data bytes. | 
 |   final iterator = ChunkedStreamReader(source); | 
 |  | 
 |   while (position < size) { | 
 |     // Yield all the necessary sparse holes. | 
 |     while (sparseHoleIndex < sparseHoles.length && | 
 |         sparseHoles[sparseHoleIndex].offset == position) { | 
 |       final sparseHole = sparseHoles[sparseHoleIndex]; | 
 |       yield* zeroes(sparseHole.length); | 
 |       position += sparseHole.length; | 
 |       sparseHoleIndex++; | 
 |     } | 
 |  | 
 |     if (position == size) break; | 
 |  | 
 |     /// Yield up to the next sparse hole's offset, or all the way to the end | 
 |     /// if there are no sparse holes left. | 
 |     var yieldTo = size; | 
 |     if (sparseHoleIndex < sparseHoles.length) { | 
 |       yieldTo = sparseHoles[sparseHoleIndex].offset; | 
 |     } | 
 |  | 
 |     // Yield data as substream, but make sure that we have enough data. | 
 |     var checkedPosition = position; | 
 |     await for (final chunk in iterator.readStream(yieldTo - position)) { | 
 |       yield chunk; | 
 |       checkedPosition += chunk.length; | 
 |     } | 
 |  | 
 |     if (checkedPosition != yieldTo) { | 
 |       throw TarException('Invalid sparse data: Unexpected end of input stream'); | 
 |     } | 
 |  | 
 |     position = yieldTo; | 
 |   } | 
 | } | 
 |  | 
 | /// Reports whether [sparseEntries] is a valid sparse map. | 
 | /// It does not matter whether [sparseEntries] represents data fragments or | 
 | /// hole fragments. | 
 | bool validateSparseEntries(List<SparseEntry> sparseEntries, int size) { | 
 |   // Validate all sparse entries. These are the same checks as performed by | 
 |   // the BSD tar utility. | 
 |   if (size < 0) return false; | 
 |  | 
 |   SparseEntry? previous; | 
 |  | 
 |   for (final current in sparseEntries) { | 
 |     // Negative values are never okay. | 
 |     if (current.offset < 0 || current.length < 0) return false; | 
 |  | 
 |     // Integer overflow with large length. | 
 |     if (current.offset + current.length < current.offset) return false; | 
 |  | 
 |     // Region extends beyond the actual size. | 
 |     if (current.end > size) return false; | 
 |  | 
 |     // Regions cannot overlap and must be in order. | 
 |     if (previous != null && previous.end > current.offset) return false; | 
 |  | 
 |     previous = current; | 
 |   } | 
 |  | 
 |   return true; | 
 | } | 
 |  | 
 | /// Converts a sparse map ([source]) from one form to the other. | 
 | /// If the input is sparse holes, then it will output sparse datas and | 
 | /// vice-versa. The input must have been already validated. | 
 | /// | 
 | /// This function mutates [source] and returns a normalized map where: | 
 | ///	* adjacent fragments are coalesced together | 
 | ///	* only the last fragment may be empty | 
 | ///	* the endOffset of the last fragment is the total size | 
 | List<SparseEntry> invertSparseEntries(List<SparseEntry> source, int size) { | 
 |   final result = <SparseEntry>[]; | 
 |   var previous = SparseEntry(0, 0); | 
 |   for (final current in source) { | 
 |     /// Skip empty fragments | 
 |     if (current.length == 0) continue; | 
 |  | 
 |     final newLength = current.offset - previous.offset; | 
 |     if (newLength > 0) { | 
 |       result.add(SparseEntry(previous.offset, newLength)); | 
 |     } | 
 |  | 
 |     previous = SparseEntry(current.end, 0); | 
 |   } | 
 |   final lastLength = size - previous.offset; | 
 |   result.add(SparseEntry(previous.offset, lastLength)); | 
 |   return result; | 
 | } |