unpackFromStream static method
Restores a folder tree from a pack source stream (e.g. the authenticated
plaintext frames of PqForgeStreamCipher.decryptStream) under
outputDirPath. Every path is re-validated against traversal.
Every byte consumed has already been authenticated frame-by-frame, but a truncated archive is only detectable at stream end — so on any failure the files this call created are removed before the error propagates, leaving no partial tree behind. Returns the entry count.
Implementation
static Future<int> unpackFromStream(
Stream<Uint8List> source, {
required String outputDirPath,
}) async {
final reader = _StreamByteReader(source);
final created = <File>[];
var count = 0;
try {
while (true) {
final pathLenBytes = await reader.readExactlyOrNull(4);
if (pathLenBytes == null) break; // clean EOF at an entry boundary
final pathLen = _readUint32(pathLenBytes);
if (pathLen <= 0 || pathLen > maxPathBytes) {
throw PqForgeException('Invalid pack entry path length: $pathLen');
}
final relativePath = _decodeUtf8(await reader.readExactly(pathLen));
_requireSafeRelativePath(relativePath);
final contentLen = _readUint64(await reader.readExactly(8));
final output = File(_join(outputDirPath, relativePath));
await output.parent.create(recursive: true);
final sink = await output.open(mode: FileMode.write);
created.add(output);
try {
var remaining = contentLen;
while (remaining > 0) {
final chunk = await reader.readUpTo(remaining);
if (chunk == null) {
throw PqForgeException(
'Truncated pack content for $relativePath',
);
}
await sink.writeFrom(chunk);
remaining -= chunk.length;
}
} finally {
await sink.close();
}
count++;
}
return count;
} catch (_) {
for (final file in created) {
try {
if (file.existsSync()) await file.delete();
} on FileSystemException {
// Best effort: never mask the original failure.
}
}
rethrow;
}
}