unpackFromStream static method

Future<int> unpackFromStream(
  1. Stream<Uint8List> source, {
  2. required String outputDirPath,
})

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;
  }
}