readRange method
Read a byte range.
Returns bytes starting at start with the given length. For
encrypted files, only the overlapping chunks are decrypted; for
chunked files, only the overlapping chunk records are read.
Implementation
@override
Future<Uint8List> readRange(
String key, {
required int start,
required int length,
}) async {
checkNotDisposed();
final info = await _inner.head(key);
if (info == null) throw FileNotFoundError(key);
if (info.metadata[_metaEncrypted] != 'true') {
return _inner.readRange(key, start: start, length: length);
}
final encKey = await _keyResolver?.resolveKey(key);
if (encKey == null) throw EncryptionKeyMissingError(key);
final g = await _resolveGeometry(key, info);
// Chunk-aware range read — only decrypt overlapping chunks
final firstChunk = start ~/ g.chunkSize;
final lastChunk = (start + length - 1) ~/ g.chunkSize;
final buffer = BytesBuilder(copy: false);
for (var i = firstChunk; i <= lastChunk && i < g.chunkCount; i++) {
final isLastChunk = i == g.chunkCount - 1;
final dataSize = isLastChunk
? g.originalSize - (i * g.chunkSize)
: g.chunkSize;
final chunkTotalSize = dataSize + _encryptor.chunkMacSize;
final chunkStart =
g.headerSize + (i * (g.chunkSize + _encryptor.chunkMacSize));
final chunkData = await _inner.readRange(
key,
start: chunkStart,
length: chunkTotalSize,
);
final decrypted = await _encryptor.decryptChunk(
chunkData,
encKey,
g.nonce,
i,
);
buffer.add(decrypted);
}
// Slice to the exact requested range. Clamp the end so an over-length
// request returns the available bytes instead of throwing — matching the
// clamping every other backend does.
final chunkAlignedStart = firstChunk * g.chunkSize;
final offsetInFirstChunk = start - chunkAlignedStart;
final fullDecrypted = buffer.takeBytes();
final end = (offsetInFirstChunk + length).clamp(0, fullDecrypted.length);
return Uint8List.sublistView(fullDecrypted, offsetInFirstChunk, end);
}