unpackFromStream static method

Future<int> unpackFromStream(
  1. Stream<Uint8List> source, {
  2. required String outputDirPath,
  3. void onEntryStart(
    1. String relativePath,
    2. int contentLength
    )?,
  4. void onEntryProgress(
    1. String relativePath,
    2. int bytesProcessed,
    3. int contentLength
    )?,
  5. void onEntryDone(
    1. String relativePath
    )?,
})

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,
  void Function(String relativePath, int contentLength)? onEntryStart,
  void Function(String relativePath, int bytesProcessed, int contentLength)?
  onEntryProgress,
  void Function(String relativePath)? onEntryDone,
}) 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));
      onEntryStart?.call(relativePath, contentLen);

      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;
          onEntryProgress?.call(
            relativePath,
            contentLen - remaining,
            contentLen,
          );
        }
      } finally {
        await sink.close();
      }
      onEntryDone?.call(relativePath);
      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;
  }
}