Merge remote-tracking branch 'origin/master' into adam/write-async

# Conflicts:
#	src/SharpCompress/Compressors/LZMA/Lzma2EncoderStream.cs
#	src/SharpCompress/Factories/TarFactory.cs
#	src/SharpCompress/Polyfills/StreamExtensions.cs
#	src/SharpCompress/Writers/IWriter.cs
This commit is contained in:
Adam Hathcock
2026-05-14 16:12:13 +01:00
134 changed files with 4378 additions and 4543 deletions

View File

@@ -126,10 +126,17 @@ SharpCompress supports multiple archive and compression formats:
- See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix
### Stream Handling Rules
- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default
- Use `ReaderOptions` or `WriterOptions` with `LeaveStreamOpen = true` to control stream disposal
- **Disposal semantics**: The default `ReaderOptions.LeaveStreamOpen` value is `false`, but effective stream ownership depends on which API overload you call
- File-based overloads (e.g., `OpenArchive(string filePath)`) open the file internally and own that stream, so it is closed by default with the archive/reader
- Do **not** rely on a specific `ReaderOptions` preset being used internally; some implementations may use `ReaderOptions.ForFilePath`, while others may use default `ReaderOptions` with the same ownership semantics
- Several high-level overloads that accept a caller-provided `Stream` use external-stream semantics by default (for example, `ReaderFactory.OpenReader(Stream)` / `ArchiveFactory.OpenArchive(Stream)`), so the caller's stream is typically left open unless you opt into different ownership behavior
- Do **not** assume every stream-based overload behaves identically; some APIs require you to pass stream ownership options explicitly
- **For caller-provided streams**: When the overload accepts `ReaderOptions`, pass `ReaderOptions.ForExternalStream` or use `ReaderOptions` with `LeaveStreamOpen = true` whenever the caller must retain ownership of the stream
- Example: `var options = new ReaderOptions { LeaveStreamOpen = true };`
- Or: `var options = ReaderOptions.ForExternalStream;`
- **For file paths**: SharpCompress manages the stream lifecycle for the internally opened file stream; no manual disposal is needed beyond the archive/reader itself
- Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal
- Always dispose of readers, writers, and archives in `using` blocks
- Always dispose of readers, writers, and archives in `using` / `await using` blocks
- For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs
### Async/Await Patterns
@@ -183,6 +190,7 @@ SharpCompress supports multiple archive and compression formats:
### Validation Expectations
- Run targeted tests for the changed area first.
- On non-Windows machines, avoid net48 test runs unless Mono is installed; use framework-specific validation such as `--framework net10.0` instead.
- Run `dotnet csharpier format .` after code edits.
- Run `dotnet csharpier check .` before handing off changes.

View File

@@ -84,8 +84,8 @@ using (var archive = ZipArchive.OpenArchive("file.zip"))
archive.WriteToDirectory(@"C:\output");
// Extract single entry
var entry = archive.Entries.First();
entry.WriteToFile(@"C:\output\file.txt");
var firstEntry = archive.Entries.First();
firstEntry.WriteToFile(@"C:\output\file.txt");
// Get entry stream
using (var stream = entry.OpenEntryStream())
@@ -97,14 +97,23 @@ using (var archive = ZipArchive.OpenArchive("file.zip"))
// Async extraction (requires IAsyncArchive)
await using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip"))
{
// Extract all entries asynchronously
await asyncArchive.WriteToDirectoryAsync(
@"C:\output",
cancellationToken: cancellationToken
);
}
using (var stream = await entry.OpenEntryStreamAsync(cancellationToken))
// Open a specific entry stream asynchronously
await using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip"))
{
// ...
await foreach (var entry in asyncArchive.EntriesAsync)
{
using (var stream = await entry.OpenEntryStreamAsync(cancellationToken))
{
// ...
}
}
}
```
@@ -214,15 +223,18 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, Compressio
// Write directory
writer.WriteAll("C:\\source", "*", SearchOption.AllDirectories);
writer.WriteAll("C:\\source", "*.txt", SearchOption.TopDirectoryOnly);
// Async variants
using (var fileStream = File.OpenRead("source.txt"))
{
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
}
await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cancellationToken);
}
// Async variants: use OpenAsyncWriter to get IAsyncWriter
await using var stream = File.Create("output.zip");
await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cancellationToken);
using (var fileStream = File.OpenRead("source.txt"))
{
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
}
await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cancellationToken);
```
---
@@ -390,7 +402,7 @@ ArchiveType.ZStandard
```csharp
try
{
using (var archive = ZipArchive.Open("archive.zip",
using (var archive = ZipArchive.OpenArchive("archive.zip",
ReaderOptions.ForEncryptedArchive("password")))
{
archive.WriteToDirectory(@"C:\output");

View File

@@ -20,7 +20,7 @@
| Tar.BZip2 | BZip2 | Both | TarArchive | TarReader | TarWriter (3) |
| Tar.Zstandard | ZStandard | Decompress | TarArchive | TarReader | N/A |
| Tar.LZip | LZMA | Both | TarArchive | TarReader | TarWriter (3) |
| Tar.XZ | LZMA2 | Decompress | TarArchive | TarReader | TarWriter (3) |
| Tar.XZ | LZMA2 | Decompress | TarArchive | TarReader | N/A |
| GZip (single file) | DEFLATE | Both | GZipArchive | GZipReader | GZipWriter |
| 7Zip (4) | LZMA, LZMA2, BZip2, PPMd, BCJ, BCJ2, Deflate | Both | SevenZipArchive | N/A | SevenZipWriter |

340
docs/TAR_GAP_ANALYSIS.md Normal file
View File

@@ -0,0 +1,340 @@
# Tar Gap Analysis
## Scope
This document compares the current Tar documentation, tests, and code paths in SharpCompress.
It is intentionally implementation-focused. The goal is to identify mismatches, omissions, and incomplete areas in the current SharpCompress Tar support.
Primary references:
- `docs/FORMATS.md`
- `src/SharpCompress/Factories/TarFactory.cs`
- `src/SharpCompress/Factories/TarWrapper.cs`
- `src/SharpCompress/Archives/Tar/`
- `src/SharpCompress/Readers/Tar/`
- `src/SharpCompress/Writers/Tar/`
- `src/SharpCompress/Common/Tar/`
- `tests/SharpCompress.Test/Tar/`
## Claimed vs Actual Support
### `Tar.XZ` is read-only
`tar.xz` is supported for reading, but not for writing.
Actual implementation in `src/SharpCompress/Writers/Tar/TarWriter.cs` does not support `CompressionType.Xz`. The writer throws `InvalidFormatException` for any compression type outside:
- `None`
- `GZip`
- `BZip2`
- `LZip`
Impact:
- Tar write support is narrower than Tar read support
- `tar.xz` creation is not available through the built-in Tar writer
Recommended action:
- keep the format table marked `N/A` for Tar.XZ writer support
## Read-Path Gaps
### PAX headers are not implemented
There is no explicit support for POSIX PAX local extended headers.
Evidence:
- `EntryType` does not define the usual local PAX header type value
- `TarHeader.Read` handles `LongName` and `LongLink`, but does not implement PAX record parsing
- there are no tests or test archives covering PAX behavior
Impact:
- archives relying on PAX for long names, metadata, or timestamps may not be interpreted correctly
Recommended action:
- decide whether PAX is intentionally unsupported or should be implemented
- document that decision explicitly
### Sparse files are not semantically implemented
`EntryType` defines `SparseFile`, but the read path does not contain sparse map handling or sparse reconstruction logic.
Evidence:
- `src/SharpCompress/Common/Tar/Headers/EntryType.cs`
- no sparse-specific code in `TarHeader`, `TarEntry`, `TarFilePart`, or `TarArchive`
- no sparse tests
Impact:
- sparse entries may be treated as ordinary entries rather than sparse files with holes
Recommended action:
- document sparse support as unsupported or partial
- add explicit tests if future support is added
### Global extended headers are not semantically implemented
`EntryType` defines `GlobalExtendedHeader`, but no semantic handling exists in the read pipeline.
Evidence:
- `TarHeader.Read` does not special-case `GlobalExtendedHeader`
- `TarEntry` does not surface a global-header model
- no tests cover this case
Impact:
- global metadata records are not applied in a defined way
Recommended action:
- document as unsupported until explicit behavior exists
### Device and FIFO semantics are not surfaced
The entry type enum includes `CharDevice`, `BlockDevice`, and `Fifo`, but the public tar model does not expose device metadata semantics.
Impact:
- such entries may not round-trip meaningfully through the API
- behavior is undocumented and untested
Recommended action:
- either document them as raw/unmodeled entry types or add dedicated support
## Write-Path Gaps
### `HeaderFormat` is not honored consistently
`TarWriterOptions.HeaderFormat` exists and defaults to `GNU_TAR_LONG_LINK`, but the configured value is not consistently applied.
### Sync directory write path
`TarWriter.WriteDirectory` creates headers using:
- `new TarHeader(WriterOptions.ArchiveEncoding)`
This uses the default tar header format rather than the writer's configured `headerFormat` field.
Impact:
- directory entries written through the sync path do not follow `TarWriterOptions.HeaderFormat`
### Async write path
`TarWriter.WriteAsync` and `WriteDirectoryAsync` also create headers using the default constructor rather than the configured header format.
Impact:
- async writes ignore `TarWriterOptions.HeaderFormat` for both file and directory entries
Recommended action:
- pass the configured header format to all `TarHeader` constructions in sync and async write paths
- add tests for both `GNU_TAR_LONG_LINK` and `USTAR`
### No public link-writing support
The read path supports symbolic and hard link targets through `TarEntry.LinkTarget`, but the write API exposes only regular file and directory creation.
Impact:
- symlink and hardlink tar archives cannot be created through the current public Tar writer API
Recommended action:
- either document this as a deliberate limitation or add link-writing APIs
### Metadata round-trip support is incomplete
The writer does not round-trip rich tar metadata beyond the basic fields needed for file and directory entries.
Current write behavior sets fixed defaults for some fields such as mode, owner id, and group id.
Impact:
- modified or newly created tar archives may lose metadata fidelity relative to the original archive
Recommended action:
- document current metadata write behavior clearly
- expand metadata support only if needed by consumers
### No write support for some detected wrappers
The detection and read path supports wrappers that the write path does not support.
| Wrapper | Read support | Write support |
| ------- | ------------ | ------------- |
| `tar.xz` | Yes | No |
| `tar.zst` | Yes | No |
| `tar.Z` | Yes | No |
This is not inherently wrong, but it should be clearly documented everywhere support is summarized.
## Sync and Async API Inconsistencies
### Seekability requirements differ at the API boundary
Synchronous `TarArchive.OpenArchive(Stream)` explicitly throws if the stream is not seekable.
Asynchronous `TarArchive.OpenAsyncArchive(Stream)` does not perform the same public guard.
Impact:
- callers do not see the same contract from sync and async overloads
- behavior is harder to reason about from API docs alone
Recommended action:
- either align the contracts or document the difference explicitly
### Async and sync write behavior do not align on header format handling
This is the most visible sync/async inconsistency in the current Tar writer implementation.
Recommended action:
- fix the implementation first
- add matching sync and async tests to keep the behavior aligned
## Test Coverage Gaps
### Symlink coverage exists in test data but not in assertions
There is a tar archive containing symlinks:
- `tests/TestArchives/Archives/TarWithSymlink.tar.gz`
Current Tar tests do not assert tar symlink behavior against that fixture.
Impact:
- the code claims practical read support for link targets, but coverage does not verify it
Recommended action:
- add reader and archive tests asserting `EntryType`-derived behavior and `LinkTarget`
### No tests for `HeaderFormat`
There are currently no tests covering:
- `TarWriterOptions.HeaderFormat = USTAR`
- `TarWriterOptions.HeaderFormat = GNU_TAR_LONG_LINK`
- long-name failures in USTAR mode
- long-name success in GNU mode through the async writer path
Impact:
- the current header-format regressions were able to exist without test coverage
Recommended action:
- add dedicated sync and async writer tests for header format selection
### No tests for PAX, sparse, or global headers
There is no evidence of coverage for:
- PAX local headers
- global extended headers
- sparse tar entries
Impact:
- unsupported or partial behavior is neither documented by tests nor protected from regression
Recommended action:
- either add fixtures and tests or document these as unsupported with no test coverage
### No tests for unsupported write wrappers
There are negative tests for an invalid `Rar` compression type, but not for unsupported tar wrappers that a user might reasonably infer from read support.
Missing negative cases include:
- `CompressionType.Xz`
- `CompressionType.ZStandard`
- `CompressionType.Lzw`
Recommended action:
- add explicit negative tests so the supported write matrix stays intentional
## Documentation Gaps
### Current format documentation is too coarse for Tar
`docs/FORMATS.md` summarizes support at the wrapper level, but Tar behavior depends on more than wrapper compression.
Missing implementation-specific details include:
- GNU long-name and long-link support
- USTAR prefix handling
- oldgnu numeric quirk handling
- missing PAX support
- missing sparse support
- reader vs archive behavior differences for compressed tar
- file-size requirements for writing from non-seekable sources
Recommended action:
- keep `docs/FORMATS.md` high-level
- add and maintain a dedicated Tar spec document for details
### The current docs do not call out partial support clearly
The codebase supports some tar dialect features and not others, but the docs do not separate:
- fully supported
- partially supported
- unsupported
Recommended action:
- use an explicit feature matrix in the Tar documentation
## Recommended Follow-Ups
### Priority 0
- Correct `docs/FORMATS.md` for `Tar.XZ` write support
### Priority 1
- Fix `TarWriterOptions.HeaderFormat` handling in sync and async writer paths
- Add tests for header-format behavior
- Add symlink coverage using `TarWithSymlink.tar.gz`
### Priority 2
- Decide and document the support position for PAX headers
- Decide and document the support position for sparse files
- Decide and document the support position for global extended headers
### Priority 3
- Add negative writer tests for unsupported wrapper compressions
- Evaluate whether sync and async archive open contracts should match exactly
- Improve metadata round-trip behavior only if there is a consumer need
## Summary
The SharpCompress Tar implementation is strong on common read scenarios and basic write scenarios, but the current gaps fall into four categories:
- documentation overstating or under-describing support
- incomplete feature coverage for less common tar dialect features
- sync/async and file/directory inconsistencies in writer header-format handling
- test coverage holes around links and advanced tar metadata features
`docs/TAR_SPEC.md` should be treated as the implementation baseline. This document identifies where that baseline is incomplete, inconsistent, or incorrectly reflected elsewhere in the repository.

430
docs/TAR_SPEC.md Normal file
View File

@@ -0,0 +1,430 @@
# Tar Spec
## Scope
This document describes the Tar implementation that exists in SharpCompress today.
It is intentionally SharpCompress-specific. It documents actual behavior in the current codebase, including partial support and limitations. It is not a general tar format reference.
Primary implementation files:
- `src/SharpCompress/Factories/TarFactory.cs`
- `src/SharpCompress/Factories/TarWrapper.cs`
- `src/SharpCompress/Archives/Tar/TarArchive.cs`
- `src/SharpCompress/Archives/Tar/TarArchive.Async.cs`
- `src/SharpCompress/Archives/Tar/TarArchive.Factory.cs`
- `src/SharpCompress/Readers/Tar/TarReader.cs`
- `src/SharpCompress/Readers/Tar/TarReader.Async.cs`
- `src/SharpCompress/Writers/Tar/TarWriter.cs`
- `src/SharpCompress/Writers/Tar/TarWriter.Async.cs`
- `src/SharpCompress/Writers/Tar/TarWriterOptions.cs`
- `src/SharpCompress/Common/Tar/Headers/TarHeader.cs`
- `src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs`
- `src/SharpCompress/Common/Tar/TarHeaderFactory.cs`
- `src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs`
## API Surface
SharpCompress exposes Tar support through four main entry points.
| Type | Role |
| ---- | ---- |
| `TarFactory` | Format detection and factory entry point for archive, reader, and writer APIs |
| `TarArchive` | Archive API for enumerating and rewriting tar archives |
| `TarReader` | Forward-only reader API for streaming tar extraction |
| `TarWriter` | Forward-only writer API for creating tar archives |
`TarWriterOptions` controls output compression, stream ownership, archive finalization, encoding, and header write format.
## Supported Wrapper Formats
Tar wrapper detection is defined by `TarWrapper.Wrappers` in `src/SharpCompress/Factories/TarWrapper.cs`.
### Supported Extensions
| Wrapper | Extensions |
| ------- | ---------- |
| Plain tar | `tar` |
| Tar + BZip2 | `tar.bz2`, `tb2`, `tbz`, `tbz2`, `tz2` |
| Tar + GZip | `tar.gz`, `taz`, `tgz` |
| Tar + ZStandard | `tar.zst`, `tar.zstd`, `tzst`, `tzstd` |
| Tar + LZip | `tar.lz` |
| Tar + XZ | `tar.xz`, `txz` |
| Tar + LZW compress | `tar.Z`, `tZ`, `taZ` |
### API Support Matrix
| Wrapper | Detection | `TarArchive` read | `TarReader` read | `TarWriter` write |
| ------- | --------- | ----------------- | ---------------- | ----------------- |
| Plain tar | Yes | Yes | Yes | Yes |
| Tar + GZip | Yes | Yes | Yes | Yes |
| Tar + BZip2 | Yes | Yes | Yes | Yes |
| Tar + LZip | Yes | Yes | Yes | Yes |
| Tar + XZ | Yes | Yes | Yes | No |
| Tar + ZStandard | Yes | Yes | Yes | No |
| Tar + LZW compress | Yes | Yes | Yes | No |
Write support is implemented in `src/SharpCompress/Writers/Tar/TarWriter.cs` and currently accepts only `CompressionType.None`, `CompressionType.GZip`, `CompressionType.BZip2`, and `CompressionType.LZip`.
## Detection Behavior
Tar detection is implemented in `TarFactory.IsArchive`, `TarFactory.IsArchiveAsync`, `TarFactory.GetCompressionType`, and `TarFactory.GetCompressionTypeAsync`.
Detection behavior is:
1. Wrap the incoming stream in `SharpCompressStream`.
2. Start recording with a rewind buffer sized from `TarWrapper.MaximumRewindBufferSize`.
3. Probe each registered wrapper in order.
4. If a wrapper matches, create a decompression stream for that wrapper.
5. Call `TarArchive.IsTarFile` or `TarArchive.IsTarFileAsync` on the decompressed stream.
6. If the tar probe succeeds, treat the stream as tar with that wrapper compression.
Implications:
- Tar detection is content-based, not extension-based.
- Wrapper detection is not sufficient by itself. The decompressed payload must also parse as tar.
- Non-seekable detection is supported through the recording and rewind mechanism.
- The largest rewind requirement currently comes from BZip2, which declares a larger minimum probe buffer in `TarWrapper`.
`TarArchive.IsTarFile` and `TarArchive.IsTarFileAsync` attempt to read a single tar header and return `false` on any exception. They also treat an all-zero empty archive block as a valid empty tar archive when the entry type is defined.
## Reader Behavior
`TarReader` is the forward-only streaming API.
Implementation files:
- `src/SharpCompress/Readers/Tar/TarReader.cs`
- `src/SharpCompress/Readers/Tar/TarReader.Async.cs`
- `src/SharpCompress/Common/Tar/TarEntry.cs`
- `src/SharpCompress/Common/Tar/TarEntry.Async.cs`
- `src/SharpCompress/Common/Tar/TarReadOnlySubStream.cs`
Reader behavior:
- The reader always enumerates entries in streaming mode.
- It works with non-seekable input streams.
- It applies decompression based on the detected wrapper compression type before parsing tar headers.
- Entry streams are backed by `TarReadOnlySubStream`.
`TarReadOnlySubStream` has an important behavior: disposing an entry stream consumes any unread entry bytes and any required 512-byte padding so that the next header can be read correctly. This is what makes skipping entries work in streaming mode.
### Reader Compression Mapping
`TarReader.RequestInitialStream` and `RequestInitialStreamAsync` map the detected wrapper to the corresponding decompression stream:
- `None`
- `BZip2`
- `GZip`
- `ZStandard`
- `LZip`
- `Xz`
- `Lzw`
### Reader Entry Semantics
For each entry, SharpCompress exposes:
- `Key` from the parsed tar name
- `LinkTarget` for symbolic and hard links
- `Size`
- `CompressedSize`
- `LastModifiedTime`
- `IsDirectory`
- `Mode`
- `UserID`
- `GroupId`
Tar entries are always reported as unencrypted and CRC is always `0`.
## Archive Behavior
`TarArchive` is the archive API.
Implementation files:
- `src/SharpCompress/Archives/Tar/TarArchive.cs`
- `src/SharpCompress/Archives/Tar/TarArchive.Async.cs`
- `src/SharpCompress/Archives/Tar/TarArchive.Factory.cs`
### Open Behavior
Synchronous `TarArchive.OpenArchive(Stream)` requires a seekable stream and throws `ArgumentException` when `CanSeek` is `false`.
`TarArchive.OpenArchive(FileInfo)` and the list-based overloads use `SourceStream` and determine wrapper compression by calling `TarFactory.GetCompressionType`.
Asynchronous `OpenAsyncArchive` overloads use `TarFactory.GetCompressionTypeAsync` and do not enforce the same explicit seekability check at the public API boundary.
### Entry Loading
`TarArchive.LoadEntries` and `LoadEntriesAsync` parse entries differently depending on wrapper compression:
- Uncompressed tar uses `StreamingMode.Seekable`.
- Wrapped tar uses `StreamingMode.Streaming` because the decompressed stream is not treated as random-access.
When seekable mode is used, the header stores `DataStartPosition`, and entries reopen data through `TarFilePart` by seeking back to the data position.
When streaming mode is used, the header stores a `PackedStream`, and entry access follows streaming semantics over the decompressed stream.
### Archive Rewrite Behavior
`TarArchive` supports creating and modifying archives through `AbstractWritableArchive`:
- add file entries
- add directory entries
- remove entries
- save to a new stream or path
Archive rewrite is implemented by enumerating the existing and new entries and writing them back out through `TarWriter`.
## Writer Behavior
`TarWriter` is the forward-only tar writer.
Implementation files:
- `src/SharpCompress/Writers/Tar/TarWriter.cs`
- `src/SharpCompress/Writers/Tar/TarWriter.Async.cs`
- `src/SharpCompress/Writers/Tar/TarWriterOptions.cs`
### Supported Output Compression
The writer supports these output compression types:
- `CompressionType.None`
- `CompressionType.GZip`
- `CompressionType.BZip2`
- `CompressionType.LZip`
Any other compression type causes `InvalidFormatException`.
### Stream Ownership
If `LeaveStreamOpen` is `true`, `TarWriter` wraps the destination in a non-disposing stream.
### File Writing
`TarWriter.Write` and `WriteAsync` write a tar header followed by file contents, then pad the payload to the next 512-byte boundary.
If the source stream is non-seekable and the caller does not supply `size`, the writer throws `ArgumentException` because tar requires the file size in the header.
### Directory Writing
`WriteDirectory` and `WriteDirectoryAsync` normalize the directory name to use forward slashes and ensure the key ends with `/`.
Empty or root-equivalent directory names are skipped.
### Archive Finalization
If `FinalizeArchiveOnClose` is `true`, disposing the writer writes two 512-byte zero blocks to terminate the archive.
If the output stream implements `IFinishable`, dispose also calls `Finish()`.
## Header Write Formats
`TarHeaderWriteFormat` is defined in `src/SharpCompress/Common/Tar/Headers/TarHeaderWriteFormat.cs`.
Supported write formats:
- `GNU_TAR_LONG_LINK`
- `USTAR`
`TarWriterOptions.HeaderFormat` defaults to `GNU_TAR_LONG_LINK`.
Current implementation behavior is narrower than the option surface suggests:
- sync file writes use the configured `HeaderFormat`
- sync directory writes currently construct the default tar header format
- async file writes currently construct the default tar header format
- async directory writes currently construct the default tar header format
In practice, this means the configured `HeaderFormat` is currently honored only by the synchronous file write path.
### GNU Long Name Write Behavior
In GNU mode, when a file name exceeds the 100-byte field, `TarHeader.WriteGnuTarLongLink` writes a synthetic long-name header using `././@LongLink` and `EntryType.LongName`, then writes the long name payload, and finally writes the actual file entry.
GNU mode also writes large file sizes using binary size encoding when the size does not fit the standard octal field.
### USTAR Write Behavior
When the synchronous file write path is configured for `USTAR`, `TarHeader.WriteUstar` attempts to split a long path into:
- the main `name` field
- the `prefix` field
If the name cannot be represented in USTAR field limits, the writer throws `InvalidFormatException` and instructs the caller to use GNU Tar format instead.
## Header Read Behavior
Tar header parsing is implemented in `TarHeader.Read` and `TarHeader.ReadAsync`.
### Implemented Read Features
| Feature | Read support |
| ------- | ------------ |
| Regular file entries | Yes |
| Directory entries | Yes |
| Symbolic link target reading | Yes |
| Hard link target reading | Yes |
| GNU long name (`L`) | Yes |
| GNU long link (`K`) | Yes |
| USTAR prefix reconstruction | Yes |
| Binary size field parsing | Yes |
| oldgnu uid/gid numeric quirk parsing | Yes |
| POSIX and signed checksum validation | Yes |
### Entry Types Recognized by the Code
`EntryType` currently declares these values in `src/SharpCompress/Common/Tar/Headers/EntryType.cs`:
- `File`
- `OldFile`
- `HardLink`
- `SymLink`
- `CharDevice`
- `BlockDevice`
- `Directory`
- `Fifo`
- `LongLink`
- `LongName`
- `SparseFile`
- `VolumeHeader`
- `GlobalExtendedHeader`
SharpCompress currently has explicit handling for only a subset of those values during read and write.
### Long Name and Long Link Reads
When `TarHeader.Read` encounters `EntryType.LongName` or `EntryType.LongLink`, it reads the payload and applies it to the next real header.
Long-name payload reads are capped at `32768` bytes to avoid memory exhaustion from malformed archives.
### Name Reconstruction
For USTAR headers, if the magic field is `ustar` and the prefix field is populated, SharpCompress reconstructs the entry name as `prefix + "/" + name`.
## Name and Metadata Handling
### Path Normalization
Writer path normalization is implemented in `TarWriter.NormalizeFilename` and `NormalizeDirectoryName`.
Behavior:
- backslashes are converted to `/`
- drive prefixes before `:` are removed
- leading and trailing `/` are trimmed for file entries
- directory entries are normalized to end with `/`
### Encoding
Tar name encoding and decoding is controlled by `IArchiveEncoding`.
- reader APIs decode names with `ReaderOptions.ArchiveEncoding`
- writer APIs encode names with `TarWriterOptions.ArchiveEncoding`
The tests include UTF-8 and code page coverage for tar name handling.
### Metadata Surface
Tar metadata currently surfaced through `TarEntry` includes:
- name
- link target
- mode
- uid
- gid
- size
- last modified time
Writer metadata is narrower. The writer sets:
- `LastModifiedTime`
- `Name`
- `Size`
- entry type for file or directory
The current writer writes fixed mode, owner id, and group id defaults rather than round-tripping full metadata.
## Async Behavior
Async tar support is provided by:
- `TarArchive.OpenAsyncArchive`
- `TarReader.OpenAsyncReader`
- `TarWriter.WriteAsync`
- `TarWriter.WriteDirectoryAsync`
- `TarHeader.ReadAsync`
- `TarHeader.WriteAsync`
The async implementations generally mirror the sync implementations while using async header parsing, decompression, and stream copy paths. The most important current exception is `TarWriterOptions.HeaderFormat`, which is not consistently honored outside the synchronous file write path.
## Known Limitations
This section documents current implementation limits, not desired future behavior.
### Write limitations
- No write support for `tar.xz`
- No write support for `tar.zst`
- No write support for `tar.Z`
- No public API for writing symbolic links or hard links
- No PAX write support
- No sparse file write support
- No device or FIFO write support
### Read limitations or partial support
- No explicit PAX local header support
- No semantic sparse file handling beyond recognizing the entry type enum value
- No semantic global extended header handling beyond recognizing the entry type enum value
- No special device or FIFO object model beyond the raw entry type information available internally
### Archive behavior limitations
- Sync archive open requires a seekable input stream
- Compressed tar archive access is not full random-access in the same sense as uncompressed seekable tar
## Test Coverage Map
Tar tests live in `tests/SharpCompress.Test/Tar/`.
Representative coverage:
| Area | Tests |
| ---- | ----- |
| Wrapper detection and reading | `TarReaderTests.cs`, `TarReaderAsyncTests.cs` |
| Archive open and rewrite | `TarArchiveTests.cs`, `TarArchiveAsyncTests.cs` |
| Writer behavior | `TarWriterTests.cs`, `TarWriterAsyncTests.cs` |
| Directory entry behavior | `TarWriterDirectoryTests.cs`, `TarArchiveDirectoryTests.cs` |
| Long-name behavior | `TarArchiveTests.cs`, `TarReaderTests.cs` |
| Corruption and broken stream handling | `TarReaderTests.cs`, `TarReaderAsyncTests.cs` |
Representative tar test archives in `tests/TestArchives/Archives/`:
- `Tar.tar`
- `Tar.tar.gz`
- `Tar.tar.bz2`
- `Tar.tar.lz`
- `Tar.tar.xz`
- `Tar.tar.zst`
- `Tar.tar.Z`
- `Tar.oldgnu.tar.gz`
- `very long filename.tar`
- `ustar with long names.tar`
- `Tar.LongPathsWithLongNameExtension.tar`
- `Tar.Empty.tar`
- `TarCorrupted.tar`
- `TarWithSymlink.tar.gz`
## Summary
SharpCompress Tar support is centered around:
- broad read support for common tar wrappers
- forward-only reader behavior for streamed extraction
- seekable archive support for uncompressed tar and archive rewrite workflows
- narrower write support than read support
- GNU long-name and USTAR write support
- partial coverage for less common tar dialect features

View File

@@ -236,48 +236,38 @@ The registry also exposes `GetCompressingProvider` (now returning `ICompressionP
**Extract single entry asynchronously:**
```C#
using (Stream stream = File.OpenRead("archive.zip"))
using (var reader = ReaderFactory.OpenReader(stream))
using Stream stream = File.OpenRead("archive.zip");
await using var reader = await ReaderFactory.OpenAsyncReader(stream, cancellationToken: cancellationToken);
while (await reader.MoveToNextEntryAsync(cancellationToken))
{
while (reader.MoveToNextEntry())
if (!reader.Entry.IsDirectory)
{
if (!reader.Entry.IsDirectory)
{
using (var entryStream = reader.OpenEntryStream())
{
using (var outputStream = File.Create("output.bin"))
{
await reader.WriteEntryToAsync(outputStream, cancellationToken);
}
}
}
using var outputStream = File.Create("output.bin");
await reader.WriteEntryToAsync(outputStream, cancellationToken);
}
}
```
**Extract all entries asynchronously:**
```C#
using (Stream stream = File.OpenRead("archive.tar.gz"))
using (var reader = ReaderFactory.OpenReader(stream))
{
await reader.WriteAllToDirectoryAsync(
@"D:\temp",
cancellationToken: cancellationToken
);
}
using Stream stream = File.OpenRead("archive.tar.gz");
await using var reader = await ReaderFactory.OpenAsyncReader(stream, cancellationToken: cancellationToken);
await reader.WriteAllToDirectoryAsync(
@"D:\temp",
cancellationToken: cancellationToken
);
```
**Open and process entry stream asynchronously:**
```C#
using (var archive = ZipArchive.OpenArchive("archive.zip"))
await using var archive = await ZipArchive.OpenAsyncArchive("archive.zip", cancellationToken: cancellationToken);
await foreach (var entry in archive.EntriesAsync)
{
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
if (!entry.IsDirectory)
{
using (var entryStream = await entry.OpenEntryStreamAsync(cancellationToken))
{
// Process the decompressed stream asynchronously
await ProcessStreamAsync(entryStream, cancellationToken);
}
using var entryStream = await entry.OpenEntryStreamAsync(cancellationToken);
// Process the decompressed stream asynchronously
await ProcessStreamAsync(entryStream, cancellationToken);
}
}
```
@@ -286,28 +276,22 @@ using (var archive = ZipArchive.OpenArchive("archive.zip"))
**Write single file asynchronously:**
```C#
using (Stream archiveStream = File.OpenWrite("output.zip"))
using (var writer = WriterFactory.OpenWriter(archiveStream, ArchiveType.Zip, CompressionType.Deflate))
{
using (Stream fileStream = File.OpenRead("input.txt"))
{
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
}
}
using Stream archiveStream = File.OpenWrite("output.zip");
await using var writer = await WriterFactory.OpenAsyncWriter(archiveStream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cancellationToken);
using Stream fileStream = File.OpenRead("input.txt");
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
```
**Write entire directory asynchronously:**
```C#
using (Stream stream = File.OpenWrite("backup.tar.gz"))
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
{
await writer.WriteAllAsync(
@"D:\files",
"*",
SearchOption.AllDirectories,
cancellationToken
);
}
using Stream stream = File.OpenWrite("backup.tar.gz");
await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip), cancellationToken);
await writer.WriteAllAsync(
@"D:\files",
"*",
SearchOption.AllDirectories,
cancellationToken
);
```
**Write with progress tracking and cancellation:**
@@ -317,17 +301,15 @@ var cts = new CancellationTokenSource();
// Set timeout or cancel from UI
cts.CancelAfter(TimeSpan.FromMinutes(5));
using (Stream stream = File.OpenWrite("archive.zip"))
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, CompressionType.Deflate))
using Stream stream = File.OpenWrite("archive.zip");
await using var writer = await WriterFactory.OpenAsyncWriter(stream, ArchiveType.Zip, new WriterOptions(CompressionType.Deflate), cts.Token);
try
{
try
{
await writer.WriteAllAsync(@"D:\data", "*", SearchOption.AllDirectories, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
await writer.WriteAllAsync(@"D:\data", "*", SearchOption.AllDirectories, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
```
@@ -335,14 +317,12 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, Compressio
**Extract from archive asynchronously:**
```C#
using (var archive = ZipArchive.OpenArchive("archive.zip"))
{
// Simple async extraction - works for all archive types
await archive.WriteToDirectoryAsync(
@"C:\output",
cancellationToken: cancellationToken
);
}
await using var archive = await ZipArchive.OpenAsyncArchive("archive.zip", cancellationToken: cancellationToken);
// Simple async extraction - works for all archive types
await archive.WriteToDirectoryAsync(
@"C:\output",
cancellationToken: cancellationToken
);
```
**Benefits of Async Operations:**

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@ public abstract partial class AbstractArchive<TEntry, TVolume> : IArchive, IAsyn
internal AbstractArchive(ArchiveType type)
{
Type = type;
ReaderOptions = new();
ReaderOptions = ReaderOptions.Default;
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(Enumerable.Empty<TVolume>());
_lazyEntries = new LazyReadOnlyCollection<TEntry>(Enumerable.Empty<TEntry>());
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(

View File

@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
@@ -34,7 +31,11 @@ public static partial class ArchiveFactory
)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncArchive(new FileInfo(filePath), options, cancellationToken);
return OpenAsyncArchive(
new FileInfo(filePath),
options ?? ReaderOptions.ForFilePath,
cancellationToken
);
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
@@ -53,20 +54,20 @@ public static partial class ArchiveFactory
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
fileInfos.NotNull(nameof(fileInfos));
var filesArray = fileInfos.ToArray();
if (filesArray.Length == 0)
var filesArray = fileInfos;
if (filesArray.Count == 0)
{
throw new ArchiveOperationException("No files to open");
}
var fileInfo = filesArray[0];
if (filesArray.Length == 1)
if (filesArray.Count == 1)
{
return await OpenAsyncArchive(fileInfo, options, cancellationToken)
.ConfigureAwait(false);
@@ -83,21 +84,15 @@ public static partial class ArchiveFactory
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
IEnumerable<Stream> streams,
IReadOnlyList<Stream> streams,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
streams.NotNull(nameof(streams));
var streamsArray = streams.ToArray();
if (streamsArray.Length == 0)
{
throw new ArchiveOperationException("No streams");
}
var streamsArray = streams.RequireReadable().RequireSeekable().ToList();
var firstStream = streamsArray[0];
if (streamsArray.Length == 1)
if (streamsArray.Count == 1)
{
return await OpenAsyncArchive(firstStream, options, cancellationToken)
.ConfigureAwait(false);
@@ -112,64 +107,4 @@ public static partial class ArchiveFactory
.OpenAsyncArchive(streamsArray, options, cancellationToken)
.ConfigureAwait(false);
}
public static ValueTask<T> FindFactoryAsync<T>(
string path,
CancellationToken cancellationToken = default
)
where T : IFactory
{
path.NotNullOrEmpty(nameof(path));
return FindFactoryAsync<T>(new FileInfo(path), cancellationToken);
}
private static async ValueTask<T> FindFactoryAsync<T>(
FileInfo finfo,
CancellationToken cancellationToken
)
where T : IFactory
{
finfo.NotNull(nameof(finfo));
using Stream stream = finfo.OpenRead();
return await FindFactoryAsync<T>(stream, cancellationToken).ConfigureAwait(false);
}
private static async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
CancellationToken cancellationToken
)
where T : IFactory
{
stream.NotNull(nameof(stream));
if (!stream.CanRead || !stream.CanSeek)
{
throw new ArgumentException("Stream should be readable and seekable");
}
var factories = Factory.Factories.OfType<T>();
var startPosition = stream.Position;
foreach (var factory in factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
if (
await factory
.IsArchiveAsync(stream, cancellationToken: cancellationToken)
.ConfigureAwait(false)
)
{
stream.Seek(startPosition, SeekOrigin.Begin);
return factory;
}
}
var extensions = string.Join(", ", factories.Select(item => item.Name));
throw new ArchiveOperationException(
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
);
}
}

View File

@@ -0,0 +1,264 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public static partial class ArchiveFactory
{
/// <summary>
/// Returns information about the archive at the given file path asynchronously,
/// or <see langword="null"/> if the file is not a recognized archive.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await GetArchiveInformationAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// Returns information about the archive at the given file path asynchronously,
/// or <see langword="null"/> if the file is not a recognized archive.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return await GetArchiveInformationAsync(
stream,
readerOptions ?? ReaderOptions.ForFilePath,
cancellationToken
)
.ConfigureAwait(false);
}
/// <summary>
/// Returns information about the archive in the given stream asynchronously,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await GetArchiveInformationAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
/// <summary>
/// Returns information about the archive in the given stream asynchronously,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<ArchiveInformation?> GetArchiveInformationAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = await TryFindFactoryAsync(
stream,
readerOptions ?? ReaderOptions.ForExternalStream,
cancellationToken
)
.ConfigureAwait(false);
return factory is null
? null
: new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory);
}
internal static ValueTask<T> FindFactoryAsync<T>(
string filePath,
CancellationToken cancellationToken = default
)
where T : IFactory
{
filePath.NotNullOrEmpty(nameof(filePath));
return FindFactoryAsync<T>(new FileInfo(filePath), cancellationToken);
}
internal static async ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
CancellationToken cancellationToken = default
)
where T : IFactory
{
fileInfo.NotNull(nameof(fileInfo));
using Stream stream = fileInfo.OpenRead();
return await FindFactoryAsync<T>(stream, cancellationToken).ConfigureAwait(false);
}
internal static async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
CancellationToken cancellationToken = default
)
where T : IFactory
{
stream.RequireReadable();
stream.RequireSeekable();
// Use the shared async detection loop over all factories. If the matched factory
// implements T we return it; otherwise (or if nothing matched) we fall through
// to the same "unsupported format" exception that the original code produced,
// listing the T-typed factories as the hint for the caller.
var factory = await TryFindFactoryAsync(stream, cancellationToken).ConfigureAwait(false);
if (factory is T typedFactory)
{
return typedFactory;
}
var extensions = string.Join(", ", Factory.Factories.OfType<T>().Select(item => item.Name));
throw new ArchiveOperationException(
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
);
}
/// <summary>
/// Async counterpart of <see cref="ArchiveFactory.TryFindFactory"/>.
/// Iterates all registered factories and returns the first one whose
/// <see cref="IFactory.IsArchiveAsync"/> recognises the stream, or <see langword="null"/>.
/// Stream position is restored to its value at entry on both success and failure.
/// </summary>
private static async ValueTask<IFactory?> TryFindFactoryAsync(
Stream stream,
CancellationToken cancellationToken
) =>
await TryFindFactoryAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
private static async ValueTask<IFactory?> TryFindFactoryAsync(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
)
{
var startPosition = stream.Position;
foreach (var factory in Factory.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
var isArchive = await factory
.IsArchiveAsync(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
if (isArchive)
{
stream.Seek(startPosition, SeekOrigin.Begin);
return factory;
}
}
stream.Seek(startPosition, SeekOrigin.Begin);
return null;
}
/// <summary>
/// Returns information about the archive at the given file path,
/// or <see langword="null"/> if the file is not a recognized archive.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
public static ArchiveInformation? GetArchiveInformation(string filePath) =>
GetArchiveInformation(filePath, ReaderOptions.ForFilePath);
/// <summary>
/// Returns information about the archive at the given file path,
/// or <see langword="null"/> if the file is not a recognized archive.
/// </summary>
/// <param name="filePath">Path to the archive file.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
public static ArchiveInformation? GetArchiveInformation(
string filePath,
ReaderOptions? readerOptions
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return GetArchiveInformation(stream, readerOptions ?? ReaderOptions.ForFilePath);
}
/// <summary>
/// Returns information about the archive in the given stream,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
public static ArchiveInformation? GetArchiveInformation(Stream stream) =>
GetArchiveInformation(stream, ReaderOptions.ForExternalStream);
/// <summary>
/// Returns information about the archive in the given stream,
/// or <see langword="null"/> if the stream is not a recognized archive.
/// </summary>
/// <param name="stream">A readable and seekable stream positioned at the start of the archive.</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
public static ArchiveInformation? GetArchiveInformation(
Stream stream,
ReaderOptions? readerOptions
)
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
return factory is null
? null
: new ArchiveInformation(factory.KnownArchiveType, factory is IArchiveFactory);
}
/// <summary>
/// Iterates all registered factories and returns the first one whose
/// <see cref="IFactory.IsArchive"/> recognises the stream, or <see langword="null"/>.
/// Stream position is restored to its value at entry on both success and failure.
/// </summary>
/// <remarks>
/// This is the shared, seekable-stream detection core used by
/// <see cref="FindFactory{T}(Stream)"/>, <see cref="IsArchive(Stream, out ArchiveType?)"/>,
/// and <see cref="GetArchiveInformation(Stream)"/>.
/// <para>
/// <see cref="ReaderFactory.OpenReader(Stream, ReaderOptions)"/> uses a separate code path
/// based on <see cref="IO.SharpCompressStream"/> rewindable buffering, which supports
/// non-seekable streams and is therefore not unified with this helper.
/// </para>
/// </remarks>
private static IFactory? TryFindFactory(Stream stream) =>
TryFindFactory(stream, ReaderOptions.ForExternalStream);
private static IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions)
{
var startPosition = stream.Position;
foreach (var factory in Factory.Factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
var isArchive = factory.IsArchive(stream, readerOptions);
if (isArchive)
{
stream.Seek(startPosition, SeekOrigin.Begin);
return factory;
}
}
stream.Seek(startPosition, SeekOrigin.Begin);
return null;
}
}

View File

@@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.Factories;
@@ -21,7 +23,7 @@ public static partial class ArchiveFactory
where TOptions : IWriterOptions
{
var factory = Factory
.Factories.OfType<IWriteableArchiveFactory<TOptions>>()
.Factories.OfType<IWritableArchiveFactory<TOptions>>()
.FirstOrDefault();
if (factory != null)
@@ -35,7 +37,7 @@ public static partial class ArchiveFactory
public static IArchive OpenArchive(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenArchive(new FileInfo(filePath), options);
return OpenArchive(new FileInfo(filePath), options ?? ReaderOptions.ForFilePath);
}
public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = null)
@@ -46,19 +48,19 @@ public static partial class ArchiveFactory
}
public static IArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? options = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var filesArray = fileInfos.ToArray();
if (filesArray.Length == 0)
var filesArray = fileInfos;
if (filesArray.Count == 0)
{
throw new ArchiveOperationException("No files to open");
}
var fileInfo = filesArray[0];
if (filesArray.Length == 1)
if (filesArray.Count == 1)
{
return OpenArchive(fileInfo, options);
}
@@ -69,17 +71,16 @@ public static partial class ArchiveFactory
return FindFactory<IMultiArchiveFactory>(fileInfo).OpenArchive(filesArray, options);
}
public static IArchive OpenArchive(IEnumerable<Stream> streams, ReaderOptions? options = null)
public static IArchive OpenArchive(IReadOnlyList<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
var streamsArray = streams.ToArray();
if (streamsArray.Length == 0)
var streamsArray = streams.RequireReadable().RequireSeekable().ToList();
if (streamsArray.Count == 0)
{
throw new ArchiveOperationException("No streams");
}
var firstStream = streamsArray[0];
if (streamsArray.Length == 1)
if (streamsArray.Count == 1)
{
return OpenArchive(firstStream, options);
}
@@ -100,11 +101,11 @@ public static partial class ArchiveFactory
archive.WriteToDirectory(destinationDirectory, options);
}
public static T FindFactory<T>(string path)
public static T FindFactory<T>(string filePath)
where T : IFactory
{
path.NotNullOrEmpty(nameof(path));
using Stream stream = File.OpenRead(path);
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return FindFactory<T>(stream);
}
@@ -119,29 +120,20 @@ public static partial class ArchiveFactory
public static T FindFactory<T>(Stream stream)
where T : IFactory
{
stream.NotNull(nameof(stream));
if (!stream.CanRead || !stream.CanSeek)
stream.RequireReadable();
stream.RequireSeekable();
// Use the shared detection loop over all factories. If the matched factory
// implements T we return it; otherwise (or if nothing matched) we fall through
// to the same "unsupported format" exception that the original code produced,
// listing the T-typed factories as the hint for the caller.
var factory = TryFindFactory(stream);
if (factory is T typedFactory)
{
throw new ArgumentException("Stream should be readable and seekable");
return typedFactory;
}
var factories = Factory.Factories.OfType<T>();
var startPosition = stream.Position;
foreach (var factory in factories)
{
stream.Seek(startPosition, SeekOrigin.Begin);
if (factory.IsArchive(stream))
{
stream.Seek(startPosition, SeekOrigin.Begin);
return factory;
}
}
var extensions = string.Join(", ", factories.Select(item => item.Name));
var extensions = string.Join(", ", Factory.Factories.OfType<T>().Select(item => item.Name));
throw new ArchiveOperationException(
$"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
@@ -149,37 +141,82 @@ public static partial class ArchiveFactory
}
public static bool IsArchive(string filePath, out ArchiveType? type)
{
return IsArchive(filePath, ReaderOptions.ForFilePath, out type);
}
public static bool IsArchive(
string filePath,
ReaderOptions? readerOptions,
out ArchiveType? type
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream s = File.OpenRead(filePath);
return IsArchive(s, out type);
return IsArchive(s, readerOptions ?? ReaderOptions.ForFilePath, out type);
}
public static bool IsArchive(Stream stream, out ArchiveType? type)
{
type = null;
stream.NotNull(nameof(stream));
return IsArchive(stream, ReaderOptions.ForExternalStream, out type);
}
if (!stream.CanRead || !stream.CanSeek)
{
throw new ArgumentException("Stream should be readable and seekable");
}
public static bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type)
{
stream.RequireReadable();
stream.RequireSeekable();
var startPosition = stream.Position;
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
type = factory?.KnownArchiveType;
return factory is not null;
}
foreach (var factory in Factory.Factories)
{
var isArchive = factory.IsArchive(stream);
stream.Position = startPosition;
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
if (isArchive)
{
type = factory.KnownArchiveType;
return true;
}
}
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return await IsArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForFilePath,
cancellationToken
)
.ConfigureAwait(false);
}
return false;
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
public static async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = await TryFindFactoryAsync(
stream,
readerOptions ?? ReaderOptions.ForExternalStream,
cancellationToken
)
.ConfigureAwait(false);
return (factory is not null, factory?.KnownArchiveType);
}
public static IEnumerable<string> GetFileParts(string part1)

View File

@@ -0,0 +1,22 @@
using SharpCompress.Common;
namespace SharpCompress.Archives;
/// <summary>
/// Contains information about a detected archive, including its type and supported capabilities.
/// </summary>
/// <remarks>
/// Use <see cref="ArchiveFactory.GetArchiveInformation(System.IO.Stream)"/> or
/// <see cref="ArchiveFactory.GetArchiveInformationAsync(System.IO.Stream,System.Threading.CancellationToken)"/>
/// to obtain an instance of this record.
/// </remarks>
/// <param name="Type">
/// The type of archive detected, or <see langword="null"/> when the format is not a registered well-known type.
/// </param>
/// <param name="SupportsRandomAccess">
/// <see langword="true"/> when this archive format supports random access via the <see cref="IArchive"/> API,
/// meaning the full file listing can be retrieved without decompressing the entire archive.
/// <see langword="false"/> when only the <see cref="SharpCompress.Readers.IReader"/> API is available,
/// which reads entries sequentially and can only report per-entry progress.
/// </param>
public record ArchiveInformation(ArchiveType? Type, bool SupportsRandomAccess);

View File

@@ -21,14 +21,14 @@ public partial class GZipArchive
#endif
{
public static ValueTask<IWritableAsyncArchive<GZipWriterOptions>> OpenAsyncArchive(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return OpenAsyncArchive(new FileInfo(path), readerOptions, cancellationToken);
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncArchive(new FileInfo(filePath), readerOptions, cancellationToken);
}
public static IWritableArchive<GZipWriterOptions> OpenArchive(
@@ -37,7 +37,7 @@ public partial class GZipArchive
)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
return OpenArchive(new FileInfo(filePath), readerOptions ?? ReaderOptions.ForFilePath);
}
public static IWritableArchive<GZipWriterOptions> OpenArchive(
@@ -50,39 +50,38 @@ public partial class GZipArchive
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IWritableArchive<GZipWriterOptions> OpenArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
var files = fileInfos;
return new GZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
i => i < files.Count ? files[i] : null,
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IWritableArchive<GZipWriterOptions> OpenArchive(
IEnumerable<Stream> streams,
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
var strms = streams.RequireReadable().RequireSeekable().ToList();
return new GZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
i => i < strms.Count ? strms[i] : null,
readerOptions ?? ReaderOptions.ForExternalStream
)
);
}
@@ -92,15 +91,11 @@ public partial class GZipArchive
ReaderOptions? readerOptions = null
)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
stream.RequireReadable();
stream.RequireSeekable();
return new GZipArchive(
new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions())
new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream)
);
}

View File

@@ -20,7 +20,7 @@ public interface IArchiveOpenable<TSync, TASync>
public static abstract TSync OpenArchive(Stream stream, ReaderOptions? readerOptions = null);
public static abstract ValueTask<TASync> OpenAsyncArchive(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);

View File

@@ -12,12 +12,12 @@ public interface IMultiArchiveOpenable<TSync, TASync>
where TASync : IAsyncArchive
{
public static abstract TSync OpenArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
);
public static abstract TSync OpenArchive(
IEnumerable<Stream> streams,
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
);

View File

@@ -1,8 +1,6 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
@@ -11,7 +9,7 @@ public static class IWritableArchiveExtensions
extension(IWritableArchive writableArchive)
{
public void AddAllFromDirectory(
string filePath,
string directoryPath,
string searchPattern = "*.*",
SearchOption searchOption = SearchOption.AllDirectories
)
@@ -19,12 +17,16 @@ public static class IWritableArchiveExtensions
using (writableArchive.PauseEntryRebuilding())
{
foreach (
var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption)
var filePath in Directory.EnumerateFiles(
directoryPath,
searchPattern,
searchOption
)
)
{
var fileInfo = new FileInfo(path);
var fileInfo = new FileInfo(filePath);
writableArchive.AddEntry(
path.Substring(filePath.Length),
filePath.Substring(directoryPath.Length),
fileInfo.OpenRead(),
true,
fileInfo.Length,

View File

@@ -3,7 +3,7 @@ using SharpCompress.Common.Options;
namespace SharpCompress.Archives;
/// <summary>
/// Decorator for <see cref="Factories.Factory"/> used to declare an archive format as able to create writeable archives
/// Decorator for <see cref="Factories.Factory"/> used to declare an archive format as able to create writable archives.
/// </summary>
/// <remarks>
/// Implemented by:<br/>
@@ -12,7 +12,8 @@ namespace SharpCompress.Archives;
/// <item><see cref="Factories.ZipFactory"/></item>
/// <item><see cref="Factories.GZipFactory"/></item>
/// </list>
public interface IWriteableArchiveFactory<TOptions> : Factories.IFactory
/// </remarks>
public interface IWritableArchiveFactory<TOptions> : Factories.IFactory
where TOptions : IWriterOptions
{
/// <summary>

View File

@@ -2,9 +2,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
@@ -13,7 +11,7 @@ public static class IWritableAsyncArchiveExtensions
extension(IWritableAsyncArchive writableArchive)
{
public async ValueTask AddAllFromDirectoryAsync(
string filePath,
string directoryPath,
string searchPattern = "*.*",
SearchOption searchOption = SearchOption.AllDirectories
)
@@ -21,13 +19,17 @@ public static class IWritableAsyncArchiveExtensions
using (writableArchive.PauseEntryRebuilding())
{
foreach (
var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption)
var filePath in Directory.EnumerateFiles(
directoryPath,
searchPattern,
searchOption
)
)
{
var fileInfo = new FileInfo(path);
var fileInfo = new FileInfo(filePath);
await writableArchive
.AddEntryAsync(
path.Substring(filePath.Length),
filePath.Substring(directoryPath.Length),
fileInfo.OpenRead(),
true,
fileInfo.Length,

View File

@@ -15,18 +15,17 @@ namespace SharpCompress.Archives.Rar;
internal class FileInfoRarArchiveVolume : RarVolume
{
internal FileInfoRarArchiveVolume(FileInfo fileInfo, ReaderOptions options, int index)
: base(StreamingMode.Seekable, fileInfo.OpenRead(), FixOptions(options), index)
: base(
StreamingMode.Seekable,
fileInfo.OpenRead(),
options.WithLeaveStreamOpen(false),
index
)
{
FileInfo = fileInfo;
FileParts = GetVolumeFileParts().ToArray().ToReadOnly();
}
private static ReaderOptions FixOptions(ReaderOptions options)
{
//make sure we're closing streams with fileinfo
return options with { LeaveStreamOpen = false };
}
internal ReadOnlyCollection<RarFilePart> FileParts { get; }
internal FileInfo FileInfo { get; }

View File

@@ -21,14 +21,14 @@ public partial class RarArchive
#endif
{
public static ValueTask<IRarAsyncArchive> OpenAsyncArchive(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IRarAsyncArchive)OpenArchive(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IRarAsyncArchive)OpenArchive(new FileInfo(filePath), readerOptions));
}
public static IRarArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
@@ -39,7 +39,7 @@ public partial class RarArchive
new SourceStream(
fileInfo,
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
@@ -51,53 +51,48 @@ public partial class RarArchive
new SourceStream(
fileInfo,
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IRarArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
stream.RequireReadable();
stream.RequireSeekable();
return new RarArchive(
new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions())
new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream)
);
}
public static IRarArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
var files = fileInfos;
return new RarArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
i => i < files.Count ? files[i] : null,
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IRarArchive OpenArchive(
IEnumerable<Stream> streams,
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
var strms = streams.RequireReadable().RequireSeekable().ToList();
return new RarArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
i => i < strms.Count ? strms[i] : null,
readerOptions ?? ReaderOptions.ForExternalStream
)
);
}
@@ -158,7 +153,7 @@ public partial class RarArchive
{
try
{
MarkHeader.Read(stream, true, false);
MarkHeader.Read(stream, true, options?.LookForHeader ?? false);
return true;
}
catch
@@ -177,7 +172,7 @@ public partial class RarArchive
try
{
await MarkHeader
.ReadAsync(stream, true, false, cancellationToken)
.ReadAsync(stream, true, options?.LookForHeader ?? false, cancellationToken)
.ConfigureAwait(false);
return true;
}

View File

@@ -17,22 +17,25 @@ public partial class SevenZipArchive
#endif
{
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty("path");
filePath.NotNullOrEmpty(nameof(filePath));
return new(
(IAsyncArchive)OpenArchive(new FileInfo(path), readerOptions ?? new ReaderOptions())
(IAsyncArchive)OpenArchive(
new FileInfo(filePath),
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty("filePath");
return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
filePath.NotNullOrEmpty(nameof(filePath));
return OpenArchive(new FileInfo(filePath), readerOptions ?? ReaderOptions.ForFilePath);
}
public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null)
@@ -42,54 +45,49 @@ public partial class SevenZipArchive
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
var files = fileInfos;
return new SevenZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
i => i < files.Count ? files[i] : null,
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IArchive OpenArchive(
IEnumerable<Stream> streams,
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
var strms = streams.RequireReadable().RequireSeekable().ToList();
return new SevenZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
i => i < strms.Count ? strms[i] : null,
readerOptions ?? ReaderOptions.ForExternalStream
)
);
}
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
stream.RequireReadable();
stream.RequireSeekable();
return new SevenZipArchive(
new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions())
new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream)
);
}
@@ -145,11 +143,14 @@ public partial class SevenZipArchive
return IsSevenZipFile(stream);
}
public static bool IsSevenZipFile(Stream stream)
public static bool IsSevenZipFile(Stream stream) =>
IsSevenZipFile(stream, ReaderOptions.ForExternalStream);
public static bool IsSevenZipFile(Stream stream, ReaderOptions? readerOptions)
{
try
{
return SignatureMatch(stream);
return SignatureMatch(stream, readerOptions?.LookForHeader ?? false);
}
catch
{
@@ -160,12 +161,25 @@ public partial class SevenZipArchive
public static async ValueTask<bool> IsSevenZipFileAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await IsSevenZipFileAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
public static async ValueTask<bool> IsSevenZipFileAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
return await SignatureMatchAsync(stream, cancellationToken).ConfigureAwait(false);
return await SignatureMatchAsync(
stream,
readerOptions?.LookForHeader ?? false,
cancellationToken
)
.ConfigureAwait(false);
}
catch
{
@@ -175,13 +189,29 @@ public partial class SevenZipArchive
private static ReadOnlySpan<byte> Signature => [(byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C];
private static bool SignatureMatch(Stream stream)
private static bool SignatureMatch(Stream stream, bool lookForHeader)
{
var buffer = ArrayPool<byte>.Shared.Rent(6);
try
{
stream.ReadExact(buffer, 0, 6);
return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature);
var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0;
for (var offset = 0; offset <= maxScanOffset; offset++)
{
stream.ReadExact(buffer, 0, 6);
if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature))
{
return true;
}
if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6)
{
return false;
}
stream.Position -= 5;
}
return false;
}
finally
{
@@ -191,18 +221,39 @@ public partial class SevenZipArchive
private static async ValueTask<bool> SignatureMatchAsync(
Stream stream,
bool lookForHeader,
CancellationToken cancellationToken
)
{
var buffer = ArrayPool<byte>.Shared.Rent(6);
try
{
if (!await stream.ReadFullyAsync(buffer, 0, 6, cancellationToken).ConfigureAwait(false))
var maxScanOffset = lookForHeader ? 0x80000 - 20 : 0;
for (var offset = 0; offset <= maxScanOffset; offset++)
{
return false;
if (
!await stream
.ReadFullyAsync(buffer, 0, 6, cancellationToken)
.ConfigureAwait(false)
)
{
return false;
}
if (buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature))
{
return true;
}
if (!lookForHeader || !stream.CanSeek || stream.Length - stream.Position < 6)
{
return false;
}
stream.Position -= 5;
}
return buffer.AsSpan().Slice(0, 6).SequenceEqual(Signature);
return false;
}
finally
{

View File

@@ -141,7 +141,7 @@ public partial class TarArchive
using (var entryStream = entry.OpenEntryStream())
{
using var memoryStream = new MemoryStream();
using var memoryStream = new PooledMemoryStream();
await entryStream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
var bytes = memoryStream.ToArray();

View File

@@ -38,23 +38,20 @@ public partial class TarArchive
)
{
fileInfo.NotNull(nameof(fileInfo));
return OpenArchive(
[fileInfo],
readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false }
);
return OpenArchive([fileInfo], readerOptions ?? ReaderOptions.ForFilePath);
}
public static IWritableArchive<TarWriterOptions> OpenArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
var files = fileInfos;
var sourceStream = new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false }
i => i < files.Count ? files[i] : null,
readerOptions ?? ReaderOptions.ForFilePath
);
var compressionType = TarFactory.GetCompressionType(
sourceStream,
@@ -65,16 +62,15 @@ public partial class TarArchive
}
public static IWritableArchive<TarWriterOptions> OpenArchive(
IEnumerable<Stream> streams,
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
var strms = streams.RequireReadable().RequireSeekable().ToList();
var sourceStream = new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
i => i < strms.Count ? strms[i] : null,
readerOptions ?? ReaderOptions.ForExternalStream
);
var compressionType = TarFactory.GetCompressionType(
sourceStream,
@@ -89,12 +85,8 @@ public partial class TarArchive
ReaderOptions? readerOptions = null
)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
stream.RequireReadable();
stream.RequireSeekable();
return OpenArchive([stream], readerOptions);
}
@@ -105,11 +97,12 @@ public partial class TarArchive
CancellationToken cancellationToken = default
)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
stream.RequireSeekable();
var sourceStream = new SourceStream(
stream,
i => null,
readerOptions ?? new ReaderOptions()
readerOptions ?? ReaderOptions.ForExternalStream
);
var compressionType = await TarFactory
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)
@@ -119,14 +112,14 @@ public partial class TarArchive
}
public static ValueTask<IWritableAsyncArchive<TarWriterOptions>> OpenAsyncArchive(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return OpenAsyncArchive(new FileInfo(path), readerOptions, cancellationToken);
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncArchive(new FileInfo(filePath), readerOptions, cancellationToken);
}
public static async ValueTask<IWritableAsyncArchive<TarWriterOptions>> OpenAsyncArchive(
@@ -137,7 +130,7 @@ public partial class TarArchive
{
cancellationToken.ThrowIfCancellationRequested();
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false };
readerOptions ??= ReaderOptions.ForFilePath;
var sourceStream = new SourceStream(fileInfo, i => null, readerOptions);
var compressionType = await TarFactory
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)
@@ -153,12 +146,11 @@ public partial class TarArchive
)
{
cancellationToken.ThrowIfCancellationRequested();
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
var strms = streams.RequireReadable().RequireSeekable().ToList();
var sourceStream = new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
i => i < strms.Count ? strms[i] : null,
readerOptions ?? ReaderOptions.ForExternalStream
);
var compressionType = await TarFactory
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)
@@ -175,11 +167,11 @@ public partial class TarArchive
{
cancellationToken.ThrowIfCancellationRequested();
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
var files = fileInfos;
var sourceStream = new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false }
i => i < files.Count ? files[i] : null,
readerOptions ?? ReaderOptions.ForFilePath
);
var compressionType = await TarFactory
.GetCompressionTypeAsync(sourceStream, sourceStream.ReaderOptions, cancellationToken)

View File

@@ -151,7 +151,7 @@ public partial class TarArchive
using (var entryStream = entry.OpenEntryStream())
{
using var memoryStream = new MemoryStream();
using var memoryStream = new PooledMemoryStream();
entryStream.CopyTo(memoryStream, Constants.BufferSize);
memoryStream.Position = 0;
var bytes = memoryStream.ToArray();

View File

@@ -45,7 +45,7 @@ public partial class ZipArchive
s = new SourceStream(
v[0].Stream,
i => i < v.Length ? v[i].Stream : null,
new ReaderOptions() { LeaveStreamOpen = true }
ReaderOptions.ForExternalStream
);
}
else

View File

@@ -41,39 +41,38 @@ public partial class ZipArchive
new SourceStream(
fileInfo,
i => ZipArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false }
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IWritableArchive<ZipWriterOptions> OpenArchive(
IEnumerable<FileInfo> fileInfos,
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
var files = fileInfos;
return new ZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions() { LeaveStreamOpen = false }
i => i < files.Count ? files[i] : null,
readerOptions ?? ReaderOptions.ForFilePath
)
);
}
public static IWritableArchive<ZipWriterOptions> OpenArchive(
IEnumerable<Stream> streams,
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
var strms = streams.RequireReadable().RequireSeekable().ToList();
return new ZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
i => i < strms.Count ? strms[i] : null,
readerOptions ?? ReaderOptions.ForExternalStream
)
);
}
@@ -83,26 +82,22 @@ public partial class ZipArchive
ReaderOptions? readerOptions = null
)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
stream.RequireReadable();
stream.RequireSeekable();
return new ZipArchive(
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
new SourceStream(stream, i => null, readerOptions ?? ReaderOptions.ForExternalStream)
);
}
public static ValueTask<IWritableAsyncArchive<ZipWriterOptions>> OpenAsyncArchive(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new((IWritableAsyncArchive<ZipWriterOptions>)OpenArchive(path, readerOptions));
return new((IWritableAsyncArchive<ZipWriterOptions>)OpenArchive(filePath, readerOptions));
}
public static ValueTask<IWritableAsyncArchive<ZipWriterOptions>> OpenAsyncArchive(

View File

@@ -86,7 +86,7 @@ public partial class ZipArchive
s = new SourceStream(
v[0].Stream,
i => i < v.Length ? v[i].Stream : null,
new ReaderOptions() { LeaveStreamOpen = true }
ReaderOptions.ForExternalStream
);
}
else

View File

@@ -45,7 +45,7 @@ public sealed record ExtractionOptions : IExtractionOptions
/// </summary>
/// <remarks>
/// <b>Breaking change:</b> Changed from field to init-only property in version 0.40.0.
/// The default handler logs a warning message.
/// If no handler is provided, symbolic links are silently skipped during extraction.
/// </remarks>
public Action<string, string>? SymbolicLinkHandler { get; init; }
@@ -58,10 +58,7 @@ public sealed record ExtractionOptions : IExtractionOptions
/// Creates a new ExtractionOptions instance with the specified overwrite behavior.
/// </summary>
/// <param name="overwrite">Whether to overwrite existing files.</param>
public ExtractionOptions(bool overwrite)
{
Overwrite = overwrite;
}
public ExtractionOptions(bool overwrite) => Overwrite = overwrite;
/// <summary>
/// Creates a new ExtractionOptions instance with the specified extraction path and overwrite behavior.
@@ -102,14 +99,4 @@ public sealed record ExtractionOptions : IExtractionOptions
/// </summary>
public static ExtractionOptions PreserveMetadata =>
new() { PreserveFileTime = true, PreserveAttributes = true };
/// <summary>
/// Default symbolic link handler that logs a warning message.
/// </summary>
public static void DefaultSymbolicLinkHandler(string sourcePath, string targetPath)
{
Console.WriteLine(
$"Could not write symlink {sourcePath} -> {targetPath}, for more information please see https://github.com/dotnet/runtime/issues/24271"
);
}
}

View File

@@ -5,11 +5,11 @@ namespace SharpCompress.Common.GZip;
public class GZipVolume : Volume
{
public GZipVolume(Stream stream, ReaderOptions? options, int index)
public GZipVolume(Stream stream, ReaderOptions options, int index)
: base(stream, options, index) { }
public GZipVolume(FileInfo fileInfo, ReaderOptions options)
: base(fileInfo.OpenRead(), options with { LeaveStreamOpen = false }) { }
: base(fileInfo.OpenRead(), options.WithLeaveStreamOpen(false)) { }
public override bool IsFirstVolume => true;

View File

@@ -1,50 +0,0 @@
using System.IO;
namespace SharpCompress.Common;
internal static class EntryExtensions
{
internal static void PreserveExtractionOptions(
this IEntry entry,
string destinationFileName,
ExtractionOptions options
)
{
if (options.PreserveFileTime || options.PreserveAttributes)
{
var nf = new FileInfo(destinationFileName);
if (!nf.Exists)
{
return;
}
// update file time to original packed time
if (options.PreserveFileTime)
{
if (entry.CreatedTime.HasValue)
{
nf.CreationTime = entry.CreatedTime.Value;
}
if (entry.LastModifiedTime.HasValue)
{
nf.LastWriteTime = entry.LastModifiedTime.Value;
}
if (entry.LastAccessedTime.HasValue)
{
nf.LastAccessTime = entry.LastAccessedTime.Value;
}
}
if (options.PreserveAttributes)
{
if (entry.Attrib.HasValue)
{
nf.Attributes = (FileAttributes)
System.Enum.ToObject(typeof(FileAttributes), entry.Attrib.Value);
}
}
}
}
}

View File

@@ -86,18 +86,7 @@ internal static partial class IEntryExtensions
options ??= new ExtractionOptions();
if (entry.LinkTarget != null)
{
if (options.SymbolicLinkHandler is not null)
{
options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget);
}
else
{
ExtractionOptions.DefaultSymbolicLinkHandler(
destinationFileName,
entry.LinkTarget
);
}
return;
options.SymbolicLinkHandler?.Invoke(destinationFileName, entry.LinkTarget);
}
else
{

View File

@@ -107,19 +107,7 @@ internal static partial class IEntryExtensions
options ??= new ExtractionOptions();
if (entry.LinkTarget != null)
{
if (options.SymbolicLinkHandler is not null)
{
options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget);
}
else
{
ExtractionOptions.DefaultSymbolicLinkHandler(
destinationFileName,
entry.LinkTarget
);
}
return;
options.SymbolicLinkHandler?.Invoke(destinationFileName, entry.LinkTarget);
}
else
{
@@ -134,5 +122,69 @@ internal static partial class IEntryExtensions
entry.PreserveExtractionOptions(destinationFileName, options);
}
}
internal void PreserveExtractionOptions(
string destinationFileName,
ExtractionOptions options
)
{
if (options.PreserveFileTime || options.PreserveAttributes)
{
var nf = new FileInfo(destinationFileName);
if (!nf.Exists)
{
return;
}
// update file time to original packed time
if (options.PreserveFileTime)
{
if (entry.CreatedTime.HasValue)
{
try
{
nf.CreationTime = entry.CreatedTime.Value;
}
catch
{
// Invalid time or the OS rejected
}
}
if (entry.LastModifiedTime.HasValue)
{
try
{
nf.LastWriteTime = entry.LastModifiedTime.Value;
}
catch
{
// Invalid time or the OS rejected
}
}
if (entry.LastAccessedTime.HasValue)
{
try
{
nf.LastAccessTime = entry.LastAccessedTime.Value;
}
catch
{
// Invalid time or the OS rejected
}
}
}
if (options.PreserveAttributes)
{
if (entry.Attrib.HasValue)
{
nf.Attributes = (FileAttributes)
Enum.ToObject(typeof(FileAttributes), entry.Attrib.Value);
}
}
}
}
}
}

View File

@@ -5,11 +5,11 @@ namespace SharpCompress.Common.Lzw;
public class LzwVolume : Volume
{
public LzwVolume(Stream stream, ReaderOptions? options, int index)
public LzwVolume(Stream stream, ReaderOptions options, int index)
: base(stream, options, index) { }
public LzwVolume(FileInfo fileInfo, ReaderOptions options)
: base(fileInfo.OpenRead(), options with { LeaveStreamOpen = false }) { }
: base(fileInfo.OpenRead(), options.WithLeaveStreamOpen(false)) { }
public override bool IsFirstVolume => true;

View File

@@ -35,7 +35,7 @@ internal class CryptKey5 : ICryptKey
)
{
var passwordBytes = Encoding.UTF8.GetBytes(password);
#if LEGACY_DOTNET || NET5_0
#if LEGACY_DOTNET
using var hmac = new HMACSHA256(passwordBytes);
var block = hmac.ComputeHash(salt);
#else
@@ -50,7 +50,7 @@ internal class CryptKey5 : ICryptKey
{
for (var i = 1; i < loop[x]; i++)
{
#if LEGACY_DOTNET || NET5_0
#if LEGACY_DOTNET
block = hmac.ComputeHash(block);
#else
block = HMACSHA256.HashData(passwordBytes, block);

View File

@@ -1182,7 +1182,7 @@ internal partial class ArchiveReader
}
else
{
_stream = new MemoryStream();
_stream = new PooledMemoryStream();
}
_rem = _db._files[index].Size;
}

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Text;
using SharpCompress.Compressors.LZMA.Utilities;
using SharpCompress.IO;
namespace SharpCompress.Common.SevenZip;
@@ -215,7 +216,7 @@ internal sealed class SevenZipFilesInfoWriter
Action<Stream> writeData
)
{
using var dataStream = new MemoryStream();
using var dataStream = new PooledMemoryStream();
writeData(dataStream);
stream.WriteByte((byte)propertyId);

View File

@@ -59,7 +59,7 @@ internal sealed partial class TarHeader
int splitIndex = -1;
for (int i = 0; i < dirSeps.Count; i++)
{
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
int count = ArchiveEncoding
.GetEncoding()
.GetByteCount(fullName.AsSpan(0, dirSeps[i]));

View File

@@ -89,7 +89,7 @@ internal sealed partial class TarHeader
int splitIndex = -1;
for (int i = 0; i < dirSeps.Count; i++)
{
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
int count = ArchiveEncoding
.GetEncoding()
.GetByteCount(fullName.AsSpan(0, dirSeps[i]));

View File

@@ -11,10 +11,10 @@ public abstract partial class Volume : IVolume, IAsyncDisposable
private readonly Stream _baseStream;
private readonly Stream _actualStream;
internal Volume(Stream stream, ReaderOptions? readerOptions, int index = 0)
internal Volume(Stream stream, ReaderOptions readerOptions, int index = 0)
{
Index = index;
ReaderOptions = readerOptions ?? new ReaderOptions();
ReaderOptions = readerOptions;
_baseStream = stream;
// Only rewind if it's a buffered SharpCompressStream (not passthrough)

View File

@@ -270,7 +270,7 @@ internal sealed class Lzma2EncoderStream : Stream
}
using var inputMs = new MemoryStream(data.ToArray(), writable: false);
using var outputMs = new MemoryStream();
using var outputMs = new PooledMemoryStream();
encoder.Code(inputMs, outputMs, data.Length, -1, null);
@@ -302,7 +302,7 @@ internal sealed class Lzma2EncoderStream : Stream
decoder.SetDecoderProperties(props);
using var input = new MemoryStream(compressedData);
using var output = new MemoryStream();
using var output = new PooledMemoryStream();
decoder.Code(input, output, compressedData.Length, uncompressedSize, null);
return (int)input.Position;

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using SharpCompress.Compressors.LZMA.RangeCoder;
using SharpCompress.Compressors.PPMd.H;
using SharpCompress.Compressors.PPMd.I1;
using SharpCompress.IO;
namespace SharpCompress.Compressors.PPMd;
@@ -179,7 +180,7 @@ public class PpmdStream : Stream, IAsyncDisposable
{
if (_compress)
{
_model.EncodeBlock(_stream, new MemoryStream(), true);
_model.EncodeBlock(_stream, Stream.Null, true);
}
}
base.Dispose(disposing);

View File

@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
@@ -32,12 +31,12 @@ internal partial class RarBLAKE2spStream : RarStream
.ConfigureAwait(false);
if (result != 0)
{
Update(_blake2sp, new ReadOnlySpan<byte>(buffer, offset, result), result);
Update(_blake2sp!, new ReadOnlySpan<byte>(buffer, offset, result));
}
else
{
_hash = Final(_blake2sp);
if (!disableCRCCheck && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) && count != 0)
EnsureHash();
if (!disableCRCCheck && !GetCrc().SequenceEqual(readStream.CurrentCrc) && count != 0)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");
@@ -56,14 +55,14 @@ internal partial class RarBLAKE2spStream : RarStream
var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (result != 0)
{
Update(_blake2sp, buffer.Span.Slice(0, result), result);
Update(_blake2sp!, buffer.Span.Slice(0, result));
}
else
{
_hash = Final(_blake2sp);
EnsureHash();
if (
!disableCRCCheck
&& !(GetCrc().SequenceEqual(readStream.CurrentCrc))
&& !GetCrc().SequenceEqual(readStream.CurrentCrc)
&& buffer.Length != 0
)
{

View File

@@ -1,8 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using SharpCompress.Common;
using SharpCompress.Common.Rar.Headers;
@@ -13,14 +10,14 @@ internal partial class RarBLAKE2spStream : RarStream
private readonly MultiVolumeReadOnlyStreamBase readStream;
private readonly bool disableCRCCheck;
const uint BLAKE2S_NUM_ROUNDS = 10;
const uint BLAKE2S_FINAL_FLAG = (~(uint)0);
const int BLAKE2S_BLOCK_SIZE = 64;
const int BLAKE2S_DIGEST_SIZE = 32;
const int BLAKE2SP_PARALLEL_DEGREE = 8;
const uint BLAKE2S_INIT_IV_SIZE = 8;
private const int BLAKE2S_NUM_ROUNDS = 10;
private const uint BLAKE2S_FINAL_FLAG = ~(uint)0;
private const int BLAKE2S_BLOCK_SIZE = 64;
private const int BLAKE2S_DIGEST_SIZE = 32;
private const int BLAKE2SP_PARALLEL_DEGREE = 8;
private const int BLAKE2S_INIT_IV_SIZE = 8;
static readonly UInt32[] k_BLAKE2S_IV =
private static readonly uint[] k_BLAKE2S_IV =
{
0x6A09E667U,
0xBB67AE85U,
@@ -32,7 +29,7 @@ internal partial class RarBLAKE2spStream : RarStream
0x5BE0CD19U,
};
static readonly byte[][] k_BLAKE2S_Sigma =
private static readonly byte[][] k_BLAKE2S_Sigma =
{
new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
new byte[] { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 },
@@ -46,14 +43,14 @@ internal partial class RarBLAKE2spStream : RarStream
new byte[] { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 },
};
internal class BLAKE2S
private sealed class BLAKE2S
{
internal UInt32[] h;
internal UInt32[] t;
internal UInt32[] f;
internal byte[] b;
internal readonly uint[] h;
internal readonly uint[] t;
internal readonly uint[] f;
internal readonly byte[] b;
internal int bufferPosition;
internal UInt32 lastNodeFlag;
internal uint lastNodeFlag;
public BLAKE2S()
{
@@ -64,9 +61,9 @@ internal partial class RarBLAKE2spStream : RarStream
}
};
internal class BLAKE2SP
private sealed class BLAKE2SP
{
internal BLAKE2S[] S;
internal readonly BLAKE2S[] S;
internal int bufferPosition;
public BLAKE2SP()
@@ -79,9 +76,8 @@ internal partial class RarBLAKE2spStream : RarStream
}
};
BLAKE2SP _blake2sp;
byte[] _hash = [];
private BLAKE2SP? _blake2sp;
private byte[]? _hash;
private RarBLAKE2spStream(
IRarUnpack unpack,
@@ -92,10 +88,9 @@ internal partial class RarBLAKE2spStream : RarStream
{
this.readStream = readStream;
// TODO: rar uses a modified hash xor'ed with encryption key?
disableCRCCheck = fileHeader.IsEncrypted;
_hash = fileHeader.FileCrc.NotNull();
_blake2sp = new BLAKE2SP();
ResetCrc();
this._blake2sp = CreateBlake2sp();
}
public static RarBLAKE2spStream Create(
@@ -111,19 +106,15 @@ internal partial class RarBLAKE2spStream : RarStream
// Async methods moved to RarBLAKE2spStream.Async.cs
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
public byte[] GetCrc() =>
this._hash
?? throw new InvalidOperationException(
"hash not computed, has the stream been fully drained?"
);
public byte[] GetCrc() => _hash;
internal void ResetCrc(BLAKE2S hash)
private static void ResetCrc(BLAKE2S hash)
{
for (UInt32 j = 0; j < BLAKE2S_INIT_IV_SIZE; j++)
{
hash.h[j] = k_BLAKE2S_IV[j];
}
k_BLAKE2S_IV.AsSpan().CopyTo(hash.h);
hash.t[0] = 0;
hash.t[1] = 0;
hash.f[0] = 0;
@@ -132,14 +123,14 @@ internal partial class RarBLAKE2spStream : RarStream
hash.lastNodeFlag = 0;
}
internal void G(
ref UInt32[] m,
ref byte[] sigma,
private static void G(
Span<uint> m,
byte[] sigma,
int i,
ref UInt32 a,
ref UInt32 b,
ref UInt32 c,
ref UInt32 d
ref uint a,
ref uint b,
ref uint c,
ref uint d
)
{
a += b + m[sigma[2 * i]];
@@ -157,16 +148,22 @@ internal partial class RarBLAKE2spStream : RarStream
b = (b >> 7) | (b << 25);
}
internal void Compress(BLAKE2S hash)
private static void Compress(BLAKE2S hash)
{
var m = new UInt32[16];
var v = new UInt32[16];
for (var i = 0; i < 16; i++)
Span<uint> m = stackalloc uint[16];
if (BitConverter.IsLittleEndian)
{
m[i] = BitConverter.ToUInt32(hash.b, i * 4);
MemoryMarshal.Cast<byte, uint>(hash.b).CopyTo(m);
}
else
{
for (var i = 0; i < 16; i++)
{
m[i] = BitConverter.ToUInt32(hash.b, i * 4);
}
}
Span<uint> v = stackalloc uint[16];
for (var i = 0; i < 8; i++)
{
v[i] = hash.h[i];
@@ -184,16 +181,15 @@ internal partial class RarBLAKE2spStream : RarStream
for (var r = 0; r < BLAKE2S_NUM_ROUNDS; r++)
{
ref byte[] sigma = ref k_BLAKE2S_Sigma[r];
G(ref m, ref sigma, 0, ref v[0], ref v[4], ref v[8], ref v[12]);
G(ref m, ref sigma, 1, ref v[1], ref v[5], ref v[9], ref v[13]);
G(ref m, ref sigma, 2, ref v[2], ref v[6], ref v[10], ref v[14]);
G(ref m, ref sigma, 3, ref v[3], ref v[7], ref v[11], ref v[15]);
G(ref m, ref sigma, 4, ref v[0], ref v[5], ref v[10], ref v[15]);
G(ref m, ref sigma, 5, ref v[1], ref v[6], ref v[11], ref v[12]);
G(ref m, ref sigma, 6, ref v[2], ref v[7], ref v[8], ref v[13]);
G(ref m, ref sigma, 7, ref v[3], ref v[4], ref v[9], ref v[14]);
var sigma = k_BLAKE2S_Sigma[r];
G(m, sigma, 0, ref v[0], ref v[4], ref v[8], ref v[12]);
G(m, sigma, 1, ref v[1], ref v[5], ref v[9], ref v[13]);
G(m, sigma, 2, ref v[2], ref v[6], ref v[10], ref v[14]);
G(m, sigma, 3, ref v[3], ref v[7], ref v[11], ref v[15]);
G(m, sigma, 4, ref v[0], ref v[5], ref v[10], ref v[15]);
G(m, sigma, 5, ref v[1], ref v[6], ref v[11], ref v[12]);
G(m, sigma, 6, ref v[2], ref v[7], ref v[8], ref v[13]);
G(m, sigma, 7, ref v[3], ref v[4], ref v[9], ref v[14]);
}
for (var i = 0; i < 8; i++)
@@ -202,103 +198,124 @@ internal partial class RarBLAKE2spStream : RarStream
}
}
internal void Update(BLAKE2S hash, ReadOnlySpan<byte> data, int size)
private static void Update(BLAKE2S hash, ReadOnlySpan<byte> data)
{
var i = 0;
while (size != 0)
while (data.Length != 0)
{
var pos = hash.bufferPosition;
var reminder = BLAKE2S_BLOCK_SIZE - pos;
if (size <= reminder)
var chunkSize = BLAKE2S_BLOCK_SIZE - pos;
if (data.Length <= chunkSize)
{
data.Slice(i, size).CopyTo(new Span<byte>(hash.b, pos, size));
hash.bufferPosition += size;
data.CopyTo(hash.b.AsSpan(pos));
hash.bufferPosition += data.Length;
return;
}
data.Slice(i, reminder).CopyTo(new Span<byte>(hash.b, pos, reminder));
data.Slice(0, chunkSize).CopyTo(hash.b.AsSpan(pos));
hash.t[0] += BLAKE2S_BLOCK_SIZE;
hash.t[1] += hash.t[0] < BLAKE2S_BLOCK_SIZE ? 1U : 0U;
Compress(hash);
hash.bufferPosition = 0;
i += reminder;
size -= reminder;
data = data.Slice(chunkSize);
}
}
internal byte[] Final(BLAKE2S hash)
private static void Final(BLAKE2S hash, Span<byte> output)
{
hash.t[0] += (uint)hash.bufferPosition;
hash.t[1] += hash.t[0] < hash.bufferPosition ? 1U : 0U;
hash.f[0] = BLAKE2S_FINAL_FLAG;
hash.f[1] = hash.lastNodeFlag;
Array.Clear(hash.b, hash.bufferPosition, BLAKE2S_BLOCK_SIZE - hash.bufferPosition);
hash.b.AsSpan(hash.bufferPosition).Clear();
Compress(hash);
var mem = new MemoryStream();
for (var i = 0; i < 8; i++)
if (BitConverter.IsLittleEndian)
{
mem.Write(BitConverter.GetBytes(hash.h[i]), 0, 4);
MemoryMarshal.Cast<uint, byte>(hash.h).CopyTo(output);
}
else
{
for (var i = 0; i < 8; i++)
{
var v = hash.h[i];
output[i * 4] = (byte)v;
output[i * 4 + 1] = (byte)(v >> 8);
output[i * 4 + 2] = (byte)(v >> 16);
output[i * 4 + 3] = (byte)(v >> 24);
}
}
return mem.ToArray();
}
public void ResetCrc()
private static BLAKE2SP CreateBlake2sp()
{
_blake2sp.bufferPosition = 0;
var blake2sp = new BLAKE2SP();
for (UInt32 i = 0; i < BLAKE2SP_PARALLEL_DEGREE; i++)
for (var i = 0; i < BLAKE2SP_PARALLEL_DEGREE; i++)
{
_blake2sp.S[i].bufferPosition = 0;
ResetCrc(_blake2sp.S[i]);
_blake2sp.S[i].h[0] ^= (BLAKE2S_DIGEST_SIZE | BLAKE2SP_PARALLEL_DEGREE << 16 | 2 << 24);
_blake2sp.S[i].h[2] ^= i;
_blake2sp.S[i].h[3] ^= (BLAKE2S_DIGEST_SIZE << 24);
var blake2S = blake2sp.S[i];
ResetCrc(blake2S);
var h = blake2S.h;
// word[0]: digest_length | (fanout<<16) | (depth<<24)
h[0] ^= BLAKE2S_DIGEST_SIZE | (BLAKE2SP_PARALLEL_DEGREE << 16) | (2 << 24);
// word[2]: node_offset = leaf index
h[2] ^= (uint)i;
// word[3]: inner_length in bits 24-31
h[3] ^= BLAKE2S_DIGEST_SIZE << 24;
}
_blake2sp.S[BLAKE2SP_PARALLEL_DEGREE - 1].lastNodeFlag = BLAKE2S_FINAL_FLAG;
blake2sp.S[BLAKE2SP_PARALLEL_DEGREE - 1].lastNodeFlag = BLAKE2S_FINAL_FLAG;
return blake2sp;
}
internal void Update(BLAKE2SP hash, ReadOnlySpan<byte> data, int size)
private static void Update(BLAKE2SP hash, ReadOnlySpan<byte> data)
{
var i = 0;
var pos = hash.bufferPosition;
while (size != 0)
while (data.Length != 0)
{
var index = pos / BLAKE2S_BLOCK_SIZE;
var reminder = BLAKE2S_BLOCK_SIZE - (pos & (BLAKE2S_BLOCK_SIZE - 1));
if (reminder > size)
var chunkSize = BLAKE2S_BLOCK_SIZE - (pos & (BLAKE2S_BLOCK_SIZE - 1));
if (chunkSize > data.Length)
{
reminder = size;
chunkSize = data.Length;
}
// Update(hash.S[index], data, size);
Update(hash.S[index], data.Slice(i, reminder), reminder);
size -= reminder;
i += reminder;
pos += reminder;
pos &= (BLAKE2S_BLOCK_SIZE * (BLAKE2SP_PARALLEL_DEGREE - 1));
Update(hash.S[index], data.Slice(0, chunkSize));
data = data.Slice(chunkSize);
pos = (pos + chunkSize) & (BLAKE2S_BLOCK_SIZE * BLAKE2SP_PARALLEL_DEGREE - 1);
}
hash.bufferPosition = pos;
}
internal byte[] Final(BLAKE2SP hash)
private static byte[] Final(BLAKE2SP blake2sp)
{
var h = new BLAKE2S();
var blake2s = new BLAKE2S();
ResetCrc(blake2s);
ResetCrc(h);
h.h[0] ^= (BLAKE2S_DIGEST_SIZE | BLAKE2SP_PARALLEL_DEGREE << 16 | 2 << 24);
h.h[3] ^= (1 << 16 | BLAKE2S_DIGEST_SIZE << 24);
h.lastNodeFlag = BLAKE2S_FINAL_FLAG;
var h = blake2s.h;
// word[0]: digest_length | (fanout<<16) | (depth<<24) — same as leaves
h[0] ^= BLAKE2S_DIGEST_SIZE | (BLAKE2SP_PARALLEL_DEGREE << 16) | (2 << 24);
// word[3]: node_depth=1 (bits 16-23), inner_length=32 (bits 24-31)
h[3] ^= (1 << 16) | (BLAKE2S_DIGEST_SIZE << 24);
blake2s.lastNodeFlag = BLAKE2S_FINAL_FLAG;
Span<byte> digest = stackalloc byte[BLAKE2S_DIGEST_SIZE];
for (var i = 0; i < BLAKE2SP_PARALLEL_DEGREE; i++)
{
var digest = Final(_blake2sp.S[i]);
Update(h, digest, BLAKE2S_DIGEST_SIZE);
Final(blake2sp.S[i], digest);
Update(blake2s, digest);
}
return Final(h);
Final(blake2s, digest);
return digest.ToArray();
}
private void EnsureHash()
{
if (this._hash == null)
{
this._hash = Final(this._blake2sp!);
// prevent incorrect usage past hash finality by failing fast
this._blake2sp = null;
}
}
public override int Read(byte[] buffer, int offset, int count)
@@ -306,12 +323,12 @@ internal partial class RarBLAKE2spStream : RarStream
var result = base.Read(buffer, offset, count);
if (result != 0)
{
Update(_blake2sp, new ReadOnlySpan<byte>(buffer, offset, result), result);
Update(this._blake2sp!, new ReadOnlySpan<byte>(buffer, offset, result));
}
else
{
_hash = Final(_blake2sp);
if (!disableCRCCheck && !(GetCrc().SequenceEqual(readStream.CurrentCrc)) && count != 0)
EnsureHash();
if (!disableCRCCheck && !GetCrc().SequenceEqual(readStream.CurrentCrc) && count != 0)
{
// NOTE: we use the last FileHeader in a multipart volume to check CRC
throw new InvalidFormatException("file crc mismatch");

View File

@@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Compressors.RLE90;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Squeezed;
@@ -54,14 +55,14 @@ public partial class SqueezeStream
if (bytesRead != 2)
{
return new MemoryStream(Array.Empty<byte>());
return new PooledMemoryStream();
}
int numnodes = numNodesBytes[0] | (numNodesBytes[1] << 8);
if (numnodes >= NUMVALS || numnodes == 0)
{
return new MemoryStream(Array.Empty<byte>());
return new PooledMemoryStream();
}
var dnode = new int[numnodes, 2];
@@ -82,7 +83,7 @@ public partial class SqueezeStream
}
var bitReader = new BitReader(_stream);
var huffmanDecoded = new MemoryStream();
var huffmanDecoded = new PooledMemoryStream();
int i = 0;
while (true)

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Text;
using SharpCompress.Common;
using SharpCompress.Compressors.RLE90;
using SharpCompress.IO;
namespace SharpCompress.Compressors.Squeezed;
@@ -67,7 +68,7 @@ public partial class SqueezeStream : Stream
if (numnodes >= NUMVALS || numnodes == 0)
{
return new MemoryStream(Array.Empty<byte>());
return new PooledMemoryStream();
}
var dnode = new int[numnodes, 2];
@@ -78,7 +79,7 @@ public partial class SqueezeStream : Stream
}
var bitReader = new BitReader(_stream);
var huffmanDecoded = new MemoryStream();
var huffmanDecoded = new PooledMemoryStream();
int i = 0;
while (true)

View File

@@ -5,7 +5,7 @@ using static SharpCompress.Compressors.ZStandard.UnsafeHelper;
#if NETCOREAPP3_0_OR_GREATER
using System.Runtime.Intrinsics.X86;
#endif
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
using System.Runtime.Intrinsics.Arm;
#endif
@@ -554,7 +554,7 @@ public static unsafe partial class Methods
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ZSTD_copy16(void* dst, void* src)
{
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
if (AdvSimd.IsSupported)
{
AdvSimd.Store((byte*)dst, AdvSimd.LoadVector128((byte*)src));

View File

@@ -6,7 +6,7 @@ using static SharpCompress.Compressors.ZStandard.UnsafeHelper;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
using System.Runtime.Intrinsics.Arm;
#endif
@@ -1172,7 +1172,7 @@ public static unsafe partial class Methods
{
assert(rowEntries == 16 || rowEntries == 32 || rowEntries == 64);
assert(rowEntries <= 64);
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
if (AdvSimd.IsSupported && BitConverter.IsLittleEndian)
{
if (rowEntries == 16)
@@ -1272,7 +1272,7 @@ public static unsafe partial class Methods
}
#endif
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
if (AdvSimd.IsSupported && BitConverter.IsLittleEndian)
{
if (rowEntries == 16)

View File

@@ -23,12 +23,12 @@ public class AceFactory : Factory, IReaderFactory
yield return "ace";
}
public override bool IsArchive(Stream stream, string? password = null) =>
public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
AceHeader.IsArchive(stream);
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
) => AceHeader.IsArchiveAsync(stream, cancellationToken);

View File

@@ -25,7 +25,7 @@ public class ArcFactory : Factory, IReaderFactory
yield return "arc";
}
public override bool IsArchive(Stream stream, string? password = null)
public override bool IsArchive(Stream stream, ReaderOptions readerOptions)
{
//You may have to use some(paranoid) checks to ensure that you actually are
//processing an ARC file, since other archivers also adopted the idea of putting
@@ -63,7 +63,7 @@ public class ArcFactory : Factory, IReaderFactory
public override async ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
)
{

View File

@@ -23,12 +23,12 @@ public class ArjFactory : Factory, IReaderFactory
yield return "arj";
}
public override bool IsArchive(Stream stream, string? password = null) =>
public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
ArjHeader.IsArchive(stream);
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
) => ArjHeader.IsArchiveAsync(stream, cancellationToken);

View File

@@ -54,11 +54,10 @@ public abstract class Factory : IFactory
public abstract IEnumerable<string> GetSupportedExtensions();
/// <inheritdoc/>
public abstract bool IsArchive(Stream stream, string? password = null);
public abstract bool IsArchive(Stream stream, ReaderOptions readerOptions);
public abstract ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
);
@@ -86,7 +85,7 @@ public abstract class Factory : IFactory
if (this is IReaderFactory readerFactory)
{
stream.Rewind();
if (IsArchive(stream, options.Password))
if (IsArchive(stream, options))
{
stream.Rewind(true);
reader = readerFactory.OpenReader(stream, options);
@@ -106,10 +105,7 @@ public abstract class Factory : IFactory
if (this is IReaderFactory readerFactory)
{
stream.Rewind();
if (
await IsArchiveAsync(stream, options.Password, cancellationToken)
.ConfigureAwait(false)
)
if (await IsArchiveAsync(stream, options, cancellationToken).ConfigureAwait(false))
{
stream.Rewind(true);
return await readerFactory

View File

@@ -27,7 +27,7 @@ public class GZipFactory
IMultiArchiveFactory,
IReaderFactory,
IWriterFactory,
IWriteableArchiveFactory<GZipWriterOptions>
IWritableArchiveFactory<GZipWriterOptions>
{
#region IFactory
@@ -44,13 +44,13 @@ public class GZipFactory
}
/// <inheritdoc/>
public override bool IsArchive(Stream stream, string? password = null) =>
public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
GZipArchive.IsGZipFile(stream);
/// <inheritdoc/>
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
) => GZipArchive.IsGZipFileAsync(stream, cancellationToken);
@@ -218,7 +218,7 @@ public class GZipFactory
#endregion
#region IWriteableArchiveFactory
#region IWritableArchiveFactory
/// <inheritdoc/>
public IWritableArchive<GZipWriterOptions> CreateArchive() => GZipArchive.CreateArchive();

View File

@@ -37,18 +37,18 @@ public interface IFactory
/// Returns true if the stream represents an archive of the format defined by this type.
/// </summary>
/// <param name="stream">A stream, pointing to the beginning of the archive.</param>
/// <param name="password">optional password</param>
bool IsArchive(Stream stream, string? password = null);
/// <param name="readerOptions">Options controlling archive detection.</param>
bool IsArchive(Stream stream, ReaderOptions readerOptions);
/// <summary>
/// Returns true if the stream represents an archive of the format defined by this type asynchronously.
/// </summary>
/// <param name="stream">A stream, pointing to the beginning of the archive.</param>
/// <param name="password">optional password</param>
/// <param name="readerOptions">Options controlling archive detection.</param>
/// <param name="cancellationToken">cancellation token</param>
ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
);

View File

@@ -32,13 +32,13 @@ public class LzwFactory : Factory, IReaderFactory
}
/// <inheritdoc/>
public override bool IsArchive(Stream stream, string? password = null) =>
public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
LzwStream.IsLzwStream(stream);
/// <inheritdoc/>
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
) => LzwStream.IsLzwStreamAsync(stream, cancellationToken);

View File

@@ -31,15 +31,15 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade
}
/// <inheritdoc/>
public override bool IsArchive(Stream stream, string? password = null) =>
RarArchive.IsRarFile(stream);
public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
RarArchive.IsRarFile(stream, readerOptions);
/// <inheritdoc/>
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
) => RarArchive.IsRarFileAsync(stream, cancellationToken: cancellationToken);
) => RarArchive.IsRarFileAsync(stream, readerOptions, cancellationToken);
/// <inheritdoc/>
public override FileInfo? GetFilePart(int index, FileInfo part1) =>

View File

@@ -34,15 +34,15 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, I
}
/// <inheritdoc/>
public override bool IsArchive(Stream stream, string? password = null) =>
SevenZipArchive.IsSevenZipFile(stream);
public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
SevenZipArchive.IsSevenZipFile(stream, readerOptions);
/// <inheritdoc/>
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
) => SevenZipArchive.IsSevenZipFileAsync(stream, cancellationToken);
) => SevenZipArchive.IsSevenZipFileAsync(stream, readerOptions, cancellationToken);
#endregion

View File

@@ -25,7 +25,7 @@ public class TarFactory
IMultiArchiveFactory,
IReaderFactory,
IWriterFactory,
IWriteableArchiveFactory<TarWriterOptions>
IWritableArchiveFactory<TarWriterOptions>
{
#region IFactory
@@ -48,8 +48,9 @@ public class TarFactory
}
/// <inheritdoc/>
public override bool IsArchive(Stream stream, string? password = null)
public override bool IsArchive(Stream stream, ReaderOptions readerOptions)
{
var providers = readerOptions.Providers;
var sharpCompressStream = new SharpCompressStream(stream);
sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize);
foreach (var wrapper in TarWrapper.Wrappers)
@@ -76,10 +77,11 @@ public class TarFactory
/// <inheritdoc/>
public override async ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
)
{
var providers = readerOptions.Providers;
var sharpCompressStream = new SharpCompressStream(stream);
sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize);
foreach (var wrapper in TarWrapper.Wrappers)
@@ -311,7 +313,7 @@ public class TarFactory
/// <inheritdoc/>
public IReader OpenReader(Stream stream, ReaderOptions? options)
{
options ??= new ReaderOptions();
options ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = new SharpCompressStream(stream);
sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize);
foreach (var wrapper in TarWrapper.Wrappers)
@@ -343,7 +345,7 @@ public class TarFactory
)
{
cancellationToken.ThrowIfCancellationRequested();
options ??= new ReaderOptions();
options ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = new SharpCompressStream(stream);
sharpCompressStream.StartRecording(TarWrapper.MaximumRewindBufferSize);
foreach (var wrapper in TarWrapper.Wrappers)
@@ -469,7 +471,7 @@ public class TarFactory
#endregion
#region IWriteableArchiveFactory
#region IWritableArchiveFactory
/// <inheritdoc/>
public IWritableArchive<TarWriterOptions> CreateArchive() => TarArchive.CreateArchive();

View File

@@ -21,12 +21,12 @@ internal class ZStandardFactory : Factory
yield return "zstd";
}
public override bool IsArchive(Stream stream, string? password = null) =>
public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
ZStandardStream.IsZStandard(stream);
public override ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
) => ZStandardStream.IsZStandardAsync(stream, cancellationToken);
}

View File

@@ -24,7 +24,7 @@ public class ZipFactory
IMultiArchiveFactory,
IReaderFactory,
IWriterFactory,
IWriteableArchiveFactory<ZipWriterOptions>
IWritableArchiveFactory<ZipWriterOptions>
{
#region IFactory
@@ -43,10 +43,10 @@ public class ZipFactory
}
/// <inheritdoc/>
public override bool IsArchive(Stream stream, string? password = null)
public override bool IsArchive(Stream stream, ReaderOptions readerOptions)
{
var startPosition = stream.CanSeek ? stream.Position : -1;
if (ZipArchive.IsZipFile(stream, password))
if (ZipArchive.IsZipFile(stream, readerOptions.Password))
{
return true;
}
@@ -61,7 +61,7 @@ public class ZipFactory
stream.Position = startPosition;
//test the zip (last) file of a multipart zip
if (ZipArchive.IsZipMulti(stream, password))
if (ZipArchive.IsZipMulti(stream, readerOptions.Password))
{
return true;
}
@@ -74,7 +74,7 @@ public class ZipFactory
/// <inheritdoc/>
public override async ValueTask<bool> IsArchiveAsync(
Stream stream,
string? password = null,
ReaderOptions readerOptions,
CancellationToken cancellationToken = default
)
{
@@ -84,7 +84,7 @@ public class ZipFactory
// probe for single volume zip
if (
await ZipArchive
.IsZipFileAsync(stream, password, cancellationToken)
.IsZipFileAsync(stream, readerOptions.Password, cancellationToken)
.ConfigureAwait(false)
)
{
@@ -102,7 +102,7 @@ public class ZipFactory
//test the zip (last) file of a multipart zip
if (
await ZipArchive
.IsZipMultiAsync(stream, password, cancellationToken)
.IsZipMultiAsync(stream, readerOptions.Password, cancellationToken)
.ConfigureAwait(false)
)
{
@@ -246,7 +246,7 @@ public class ZipFactory
#endregion
#region IWriteableArchiveFactory
#region IWritableArchiveFactory
/// <inheritdoc/>
public IWritableArchive<ZipWriterOptions> CreateArchive() => ZipArchive.CreateArchive();

View File

@@ -0,0 +1,701 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.IO;
/// <summary>
/// MemoryStream implementation backed by pooled byte arrays.
/// Uses <see cref="ArrayPool{T}"/> to reduce GC pressure for temporary buffers.
/// </summary>
/// <remarks>
/// This implementation is not thread-safe. Use appropriate synchronization for concurrent access.
/// Buffers exposed via <see cref="GetBuffer"/> or <see cref="TryGetBuffer"/> are allocated as
/// fresh non-pooled arrays to avoid exposing pooled memory.
/// </remarks>
public sealed class PooledMemoryStream : MemoryStream
{
private const int MaxStreamLength = int.MaxValue;
private readonly ArrayPool<byte> _arrayPool;
private readonly int _blockSize;
private List<byte[]>? _blocks;
private bool _isOpen;
private int _position;
private int _length;
private int _capacity;
public PooledMemoryStream()
: this(0) { }
public PooledMemoryStream(int capacity)
: this(capacity, Constants.BufferSize, ArrayPool<byte>.Shared) { }
public PooledMemoryStream(int capacity, int blockSize)
: this(capacity, blockSize, ArrayPool<byte>.Shared) { }
public PooledMemoryStream(int capacity, int blockSize, ArrayPool<byte> arrayPool)
{
ThrowHelper.ThrowIfNull(arrayPool, nameof(arrayPool));
ThrowHelper.ThrowIfNegative(capacity, nameof(capacity));
ThrowHelper.ThrowIfNegativeOrZero(blockSize, nameof(blockSize));
_arrayPool = arrayPool;
_blockSize = blockSize;
_blocks = new List<byte[]>();
_isOpen = true;
_position = 0;
_length = 0;
_capacity = capacity;
EnsureSegmentedAllocated(capacity);
}
public override bool CanRead => _isOpen;
public override bool CanSeek => _isOpen;
public override bool CanWrite => _isOpen;
public override long Length
{
get
{
EnsureNotClosed();
return _length;
}
}
public override long Position
{
get
{
EnsureNotClosed();
return _position;
}
set
{
EnsureNotClosed();
ThrowHelper.ThrowIfNegative(value, nameof(value));
ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value));
_position = (int)value;
}
}
public override int Capacity
{
get
{
EnsureNotClosed();
return _capacity;
}
set
{
ThrowHelper.ThrowIfLessThan(value, _length, nameof(value));
EnsureNotClosed();
var target = value;
if (target == _capacity)
{
return;
}
SetCapacityAbsolute(target);
}
}
public override void Flush()
{
EnsureNotClosed();
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
EnsureNotClosed();
return Task.CompletedTask;
}
public override long Seek(long offset, SeekOrigin loc)
{
EnsureNotClosed();
var anchor = loc switch
{
SeekOrigin.Begin => 0,
SeekOrigin.Current => _position,
SeekOrigin.End => _length,
_ => throw new ArgumentException("Invalid seek origin.", nameof(loc)),
};
var target = anchor + offset;
if (target < 0)
{
throw new IOException("Attempted to seek before the beginning of the stream.");
}
if (target > MaxStreamLength)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
_position = (int)target;
return _position;
}
public override void SetLength(long value)
{
EnsureWritable();
ThrowHelper.ThrowIfNegative(value, nameof(value));
ThrowHelper.ThrowIfGreaterThan(value, MaxStreamLength, nameof(value));
var newLength = (int)value;
if (newLength > _capacity)
{
EnsureCapacityForAppend(newLength);
}
if (newLength > _length)
{
ClearRange(_length, newLength - _length);
}
_length = newLength;
if (_position > newLength)
{
_position = newLength;
}
}
public override int Read(byte[] buffer, int offset, int count)
{
ValidateReadWriteBufferArguments(buffer, offset, count);
EnsureNotClosed();
var available = _length - _position;
if (available <= 0)
{
return 0;
}
if (count > available)
{
count = available;
}
CopyFromSegmented(_position, buffer, offset, count);
_position += count;
return count;
}
public override int ReadByte()
{
EnsureNotClosed();
if (_position >= _length)
{
return -1;
}
var blockIndex = _position / _blockSize;
var blockOffset = _position % _blockSize;
var value = _blocks![blockIndex][blockOffset];
_position++;
return value;
}
public override void Write(byte[] buffer, int offset, int count)
{
ValidateReadWriteBufferArguments(buffer, offset, count);
EnsureWritable();
if (count == 0)
{
return;
}
var endPosition = _position + count;
if (endPosition < 0)
{
throw new IOException("Stream is too long.");
}
if (endPosition > _capacity)
{
EnsureCapacityForAppend(endPosition);
}
if (_position > _length)
{
ClearRange(_length, _position - _length);
}
CopyToSegmented(_position, buffer, offset, count);
_position = endPosition;
if (_position > _length)
{
_length = _position;
}
}
public override void WriteByte(byte value)
{
EnsureWritable();
var endPosition = _position + 1;
if (endPosition < 0)
{
throw new IOException("Stream is too long.");
}
if (endPosition > _capacity)
{
EnsureCapacityForAppend(endPosition);
}
if (_position > _length)
{
ClearRange(_length, _position - _length);
}
var blockIndex = _position / _blockSize;
var blockOffset = _position % _blockSize;
_blocks![blockIndex][blockOffset] = value;
_position = endPosition;
if (_position > _length)
{
_length = _position;
}
}
private byte[] CreateExposableBuffer()
{
var exposable = new byte[_capacity];
if (_length == 0)
{
return exposable;
}
CopyFromSegmented(0, exposable, 0, _length);
return exposable;
}
public override byte[] GetBuffer()
{
EnsureNotClosed();
return CreateExposableBuffer();
}
public override bool TryGetBuffer(out ArraySegment<byte> buffer)
{
EnsureNotClosed();
var exposableBuffer = CreateExposableBuffer();
buffer = new ArraySegment<byte>(exposableBuffer, 0, _length);
return true;
}
public override byte[] ToArray()
{
EnsureNotClosed();
var count = _length;
if (count == 0)
{
return Array.Empty<byte>();
}
var copy = new byte[count];
CopyFromSegmented(0, copy, 0, count);
return copy;
}
public override void WriteTo(Stream stream)
{
ThrowHelper.ThrowIfNull(stream, nameof(stream));
EnsureNotClosed();
var count = _length;
if (count == 0)
{
return;
}
var position = 0;
var remaining = count;
while (remaining > 0)
{
var blockIndex = position / _blockSize;
var blockOffset = position % _blockSize;
var toWrite = Math.Min(remaining, _blockSize - blockOffset);
stream.Write(_blocks![blockIndex], blockOffset, toWrite);
position += toWrite;
remaining -= toWrite;
}
}
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled<int>(cancellationToken);
}
return Task.FromResult(Read(buffer, offset, count));
}
public override Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
Write(buffer, offset, count);
return Task.CompletedTask;
}
#if !LEGACY_DOTNET
public override int Read(Span<byte> buffer)
{
EnsureNotClosed();
var available = _length - _position;
if (available <= 0)
{
return 0;
}
var count = Math.Min(available, buffer.Length);
var sourcePosition = _position;
var destinationOffset = 0;
var remaining = count;
while (remaining > 0)
{
var blockIndex = sourcePosition / _blockSize;
var blockOffset = sourcePosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
_blocks!
[blockIndex]
.AsSpan(blockOffset, toCopy)
.CopyTo(buffer.Slice(destinationOffset, toCopy));
sourcePosition += toCopy;
destinationOffset += toCopy;
remaining -= toCopy;
}
_position += count;
return count;
}
public override void Write(ReadOnlySpan<byte> buffer)
{
EnsureWritable();
if (buffer.Length == 0)
{
return;
}
var endPosition = _position + buffer.Length;
if (endPosition < 0)
{
throw new IOException("Stream is too long.");
}
if (endPosition > _capacity)
{
EnsureCapacityForAppend(endPosition);
}
if (_position > _length)
{
ClearRange(_length, _position - _length);
}
var sourceOffset = 0;
var destinationPosition = _position;
var remaining = buffer.Length;
while (remaining > 0)
{
var blockIndex = destinationPosition / _blockSize;
var blockOffset = destinationPosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
buffer
.Slice(sourceOffset, toCopy)
.CopyTo(_blocks![blockIndex].AsSpan(blockOffset, toCopy));
sourceOffset += toCopy;
destinationPosition += toCopy;
remaining -= toCopy;
}
_position = endPosition;
if (_position > _length)
{
_length = _position;
}
}
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled<int>(cancellationToken);
}
return ValueTask.FromResult(Read(buffer.Span));
}
public override ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled(cancellationToken);
}
Write(buffer.Span);
return ValueTask.CompletedTask;
}
#endif
protected override void Dispose(bool disposing)
{
if (_isOpen)
{
_isOpen = false;
if (disposing)
{
ReturnPooledBuffers();
}
}
base.Dispose(disposing);
}
private void EnsureNotClosed()
{
if (!_isOpen)
{
throw new ObjectDisposedException(nameof(PooledMemoryStream));
}
}
private void EnsureWritable()
{
EnsureNotClosed();
}
private void EnsureCapacityForAppend(int requiredLength)
{
if (requiredLength < 0)
{
throw new IOException("Stream is too long.");
}
if (requiredLength <= _capacity)
{
return;
}
var nextCapacity = RoundUpToBlockBoundary(requiredLength);
SetCapacityAbsolute(nextCapacity);
}
private void SetCapacityAbsolute(int newCapacity)
{
ThrowHelper.ThrowIfLessThan(newCapacity, _length, nameof(newCapacity));
EnsureSegmentedAllocated(newCapacity);
_capacity = newCapacity;
if (_length > _capacity)
{
_length = _capacity;
}
if (_position > _capacity)
{
_position = _capacity;
}
}
private void EnsureSegmentedAllocated(int capacity)
{
var requiredAllocated = RoundUpToBlockBoundary(capacity);
var requiredBlocks = requiredAllocated == 0 ? 0 : requiredAllocated / _blockSize;
_blocks ??= new List<byte[]>();
while (_blocks.Count < requiredBlocks)
{
_blocks.Add(_arrayPool.Rent(_blockSize));
}
while (_blocks.Count > requiredBlocks)
{
var index = _blocks.Count - 1;
var block = _blocks[index];
_blocks.RemoveAt(index);
_arrayPool.Return(block);
}
}
private int RoundUpToBlockBoundary(int value)
{
if (value <= 0)
{
return 0;
}
var rounded = ((long)value + _blockSize - 1) / _blockSize * _blockSize;
if (rounded > MaxStreamLength)
{
throw new IOException("Stream is too long.");
}
return (int)rounded;
}
private void ClearRange(int absoluteStart, int count)
{
if (count <= 0)
{
return;
}
var position = absoluteStart;
var remaining = count;
while (remaining > 0)
{
var blockIndex = position / _blockSize;
var blockOffset = position % _blockSize;
var toClear = Math.Min(remaining, _blockSize - blockOffset);
Array.Clear(_blocks![blockIndex], blockOffset, toClear);
position += toClear;
remaining -= toClear;
}
}
private void CopyFromSegmented(
int absoluteSourcePosition,
byte[] destination,
int offset,
int count
)
{
var sourcePosition = absoluteSourcePosition;
var destinationOffset = offset;
var remaining = count;
while (remaining > 0)
{
var blockIndex = sourcePosition / _blockSize;
var blockOffset = sourcePosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
Buffer.BlockCopy(
_blocks![blockIndex],
blockOffset,
destination,
destinationOffset,
toCopy
);
sourcePosition += toCopy;
destinationOffset += toCopy;
remaining -= toCopy;
}
}
private void CopyToSegmented(
int absoluteDestinationPosition,
byte[] source,
int offset,
int count
)
{
var sourceOffset = offset;
var destinationPosition = absoluteDestinationPosition;
var remaining = count;
while (remaining > 0)
{
var blockIndex = destinationPosition / _blockSize;
var blockOffset = destinationPosition % _blockSize;
var toCopy = Math.Min(remaining, _blockSize - blockOffset);
Buffer.BlockCopy(source, sourceOffset, _blocks![blockIndex], blockOffset, toCopy);
sourceOffset += toCopy;
destinationPosition += toCopy;
remaining -= toCopy;
}
}
private void ReturnSegmentedBlocks()
{
if (_blocks is null)
{
return;
}
for (var i = 0; i < _blocks.Count; i++)
{
_arrayPool.Return(_blocks[i]);
}
_blocks.Clear();
}
private void ReturnPooledBuffers()
{
ReturnSegmentedBlocks();
_blocks = null;
}
private static void ValidateReadWriteBufferArguments(byte[] buffer, int offset, int count)
{
ThrowHelper.ThrowIfNull(buffer, nameof(buffer));
ThrowHelper.ThrowIfNegative(offset, nameof(offset));
ThrowHelper.ThrowIfNegative(count, nameof(count));
if (buffer.Length - offset < count)
{
throw new ArgumentException("Offset and length are out of bounds.");
}
}
}

View File

@@ -203,11 +203,17 @@ public partial class SharpCompressStream : Stream, IStreamStack
// Allocate ring buffer with the requested minimum size (at least the global default).
if (_ringBuffer is null)
{
var size =
var requiredSize =
minBufferSize.GetValueOrDefault() > Constants.RewindableBufferSize
? minBufferSize.GetValueOrDefault()
: Constants.RewindableBufferSize;
_ringBuffer = new RingBuffer(size);
_ringBuffer = new RingBuffer(requiredSize);
}
else if (minBufferSize.HasValue && minBufferSize.Value > _ringBuffer.Capacity)
{
throw new ArchiveOperationException(
$"StartRecording requires a ring buffer of at least {minBufferSize.Value} bytes, but the stream was created with capacity {_ringBuffer.Capacity}."
);
}
// Mark current position as recording anchor

View File

@@ -28,7 +28,7 @@ public static class StreamExtensions
public async ValueTask SkipAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
#if NET5_0_OR_GREATER
#if NET6_0_OR_GREATER
await stream.CopyToAsync(Stream.Null, cancellationToken).ConfigureAwait(false);
#else
await stream.CopyToAsync(Stream.Null).ConfigureAwait(false);

View File

@@ -22,20 +22,16 @@ public abstract partial class AbstractReader<TEntry, TVolume> : IReader, IAsyncR
private bool _wroteCurrentEntry;
private readonly bool _disposeVolume;
internal AbstractReader(
ReaderOptions options,
ArchiveType archiveType,
bool disposeVolume = true
)
internal AbstractReader(ReaderOptions options, ArchiveType type, bool disposeVolume = true)
{
ArchiveType = archiveType;
Type = type;
_disposeVolume = disposeVolume;
Options = options;
}
internal ReaderOptions Options { get; }
public ArchiveType ArchiveType { get; }
public ArchiveType Type { get; }
/// <summary>
/// Current volume that the current entry resides in

View File

@@ -19,8 +19,8 @@ public partial class AceReader
/// <returns>An AceReader instance.</returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new SingleVolumeAceReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new SingleVolumeAceReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
/// <summary>
@@ -31,19 +31,19 @@ public partial class AceReader
/// <returns></returns>
public static IReader OpenReader(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeAceReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -61,8 +61,8 @@ public partial class AceReader
ReaderOptions? options = null
)
{
streams.NotNull(nameof(streams));
return new MultiVolumeAceReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -84,6 +84,7 @@ public partial class AceReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -12,7 +12,7 @@ internal class SingleVolumeAceReader : AceReader
internal SingleVolumeAceReader(Stream stream, ReaderOptions options)
: base(options)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
_stream = stream;
}

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Arc;
public partial class ArcReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -48,6 +48,7 @@ public partial class ArcReader : IReaderOpenable
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -24,8 +24,8 @@ public partial class ArcReader : AbstractReader<ArcEntry, ArcVolume>
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new ArcReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new ArcReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
protected override IEnumerable<ArcEntry> GetEntries(Stream stream)

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Arj;
public partial class ArjReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -48,6 +48,7 @@ public partial class ArjReader : IReaderOpenable
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -31,8 +31,8 @@ public abstract partial class ArjReader : AbstractReader<ArjEntry, ArjVolume>
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new SingleVolumeArjReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new SingleVolumeArjReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
/// <summary>
@@ -43,8 +43,8 @@ public abstract partial class ArjReader : AbstractReader<ArjEntry, ArjVolume>
/// <returns></returns>
public static IReader OpenReader(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeArjReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeArjReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
protected abstract void ValidateArchive(ArjVolume archive);

View File

@@ -12,7 +12,7 @@ internal class SingleVolumeArjReader : ArjReader
internal SingleVolumeArjReader(Stream stream, ReaderOptions options)
: base(options)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
_stream = stream;
}

View File

@@ -10,14 +10,14 @@ public partial class GZipReader
#endif
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -49,12 +49,13 @@ public partial class GZipReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new GZipReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new GZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
}

View File

@@ -8,7 +8,7 @@ namespace SharpCompress.Readers;
public interface IAsyncReader : IAsyncDisposable
{
ArchiveType ArchiveType { get; }
ArchiveType Type { get; }
IEntry Entry { get; }

View File

@@ -6,7 +6,7 @@ namespace SharpCompress.Readers;
public interface IReader : IDisposable
{
ArchiveType ArchiveType { get; }
ArchiveType Type { get; }
IEntry Entry { get; }

View File

@@ -24,6 +24,6 @@ public interface IReaderFactory : Factories.IFactory
ValueTask<IAsyncReader> OpenAsyncReader(
Stream stream,
ReaderOptions? options,
CancellationToken cancellationToken
CancellationToken cancellationToken = default
);
}

View File

@@ -17,7 +17,7 @@ public interface IReaderOpenable
public static abstract IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null);
public static abstract ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);

View File

@@ -10,14 +10,14 @@ public partial class LzwReader
#endif
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -49,12 +49,13 @@ public partial class LzwReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new LzwReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new LzwReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
}

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Rar;
public partial class RarReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(

View File

@@ -49,7 +49,7 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
readerOptions ??= new ReaderOptions { LeaveStreamOpen = false };
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
@@ -60,7 +60,7 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
public static IReader OpenReader(IEnumerable<FileInfo> fileInfos, ReaderOptions? options = null)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
options ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfos.Select(x => x.OpenRead()), options);
}
@@ -72,8 +72,8 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new SingleVolumeRarReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new SingleVolumeRarReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
/// <summary>
@@ -84,8 +84,8 @@ public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolu
/// <returns></returns>
public static IReader OpenReader(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
return new MultiVolumeRarReader(streams, options ?? new ReaderOptions());
var streamArray = streams.RequireReadable();
return new MultiVolumeRarReader(streamArray, options ?? ReaderOptions.ForExternalStream);
}
protected override IEnumerable<RarReaderEntry> GetEntries(Stream stream)

View File

@@ -25,7 +25,11 @@ public static partial class ReaderFactory
)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncReader(new FileInfo(filePath), options, cancellationToken);
return OpenAsyncReader(
new FileInfo(filePath),
options ?? ReaderOptions.ForFilePath,
cancellationToken
);
}
/// <summary>
@@ -52,7 +56,7 @@ public static partial class ReaderFactory
CancellationToken cancellationToken = default
)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
options ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(

View File

@@ -12,7 +12,7 @@ public static partial class ReaderFactory
public static IReader OpenReader(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenReader(new FileInfo(filePath), options);
return OpenReader(new FileInfo(filePath), options ?? ReaderOptions.ForFilePath);
}
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? options = null)
@@ -29,7 +29,7 @@ public static partial class ReaderFactory
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? options = null)
{
stream.NotNull(nameof(stream));
stream.RequireReadable();
options ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(

View File

@@ -24,9 +24,36 @@ namespace SharpCompress.Readers;
public sealed record ReaderOptions : IReaderOptions
{
/// <summary>
/// SharpCompress will keep the supplied streams open. Default is true.
/// Whether SharpCompress leaves the supplied streams open when the reader/archive is disposed.
/// As of v0.21, the library is documented to close streams by default; this option now defaults to false.
/// Set to true when passing caller-owned streams that should not be disposed.
/// </summary>
public bool LeaveStreamOpen { get; init; } = true;
/// <remarks>
/// <para>
/// <b>Default behavior (LeaveStreamOpen = false):</b>
/// When you open an archive from a file path (e.g., <c>GZipArchive.OpenArchive(filePath)</c>),
/// SharpCompress manages the stream lifetime and closes it on Dispose.
/// </para>
/// <para>
/// <b>Caller-provided streams (LeaveStreamOpen = true):</b>
/// When you pass a stream you created (FileStream, MemoryStream, NetworkStream, etc.),
/// set LeaveStreamOpen = true to prevent SharpCompress from disposing it.
/// Use <see cref="ForExternalStream"/> preset for convenience.
/// </para>
/// <para>
/// <b>Example:</b>
/// <code>
/// // File-based: stream managed by library
/// using var archive = GZipArchive.OpenArchive(filePath); // LeaveStreamOpen = false
///
/// // Caller-provided stream: caller manages lifetime
/// using var stream = File.OpenRead(filePath);
/// var options = new ReaderOptions { LeaveStreamOpen = true };
/// using var archive = GZipArchive.OpenArchive(stream, options);
/// </code>
/// </para>
/// </remarks>
public bool LeaveStreamOpen { get; init; } = false;
/// <summary>
/// Encoding to use for archive entry names.
@@ -124,35 +151,34 @@ public sealed record ReaderOptions : IReaderOptions
/// <summary>
/// Gets ReaderOptions configured for caller-provided streams.
/// </summary>
public static ReaderOptions ForExternalStream => new() { LeaveStreamOpen = true };
internal static ReaderOptions Default => new();
public static ReaderOptions ForExternalStream => Default.WithLeaveStreamOpen(true);
/// <summary>
/// Gets ReaderOptions configured for file-based overloads that open their own stream.
/// </summary>
public static ReaderOptions ForFilePath => new() { LeaveStreamOpen = false };
public static ReaderOptions ForFilePath => Default;
/// <summary>
/// Creates ReaderOptions for reading encrypted archives.
/// </summary>
/// <param name="password">The password for encrypted archives.</param>
public static ReaderOptions ForEncryptedArchive(string? password = null) =>
new ReaderOptions().WithPassword(password);
Default.WithPassword(password);
/// <summary>
/// Creates ReaderOptions for archives with custom character encoding.
/// </summary>
/// <param name="encoding">The encoding for archive entry names.</param>
public static ReaderOptions ForEncoding(IArchiveEncoding encoding) =>
new ReaderOptions().WithArchiveEncoding(encoding);
Default.WithArchiveEncoding(encoding);
/// <summary>
/// Creates ReaderOptions for self-extracting archives that require header search.
/// </summary>
public static ReaderOptions ForSelfExtractingArchive(string? password = null) =>
new ReaderOptions()
.WithLookForHeader(true)
.WithPassword(password)
.WithRewindableBufferSize(1_048_576); // 1MB for SFX archives
Default.WithLookForHeader(true).WithPassword(password).WithRewindableBufferSize(1_048_576); // 1MB for SFX archives
// Note: Parameterized constructors have been removed.
// Use fluent With*() helpers or object initializers instead:

View File

@@ -72,13 +72,13 @@ public partial class TarReader
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
path.NotNullOrEmpty(nameof(path));
return OpenAsyncReader(new FileInfo(path), readerOptions, cancellationToken);
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncReader(new FileInfo(filePath), readerOptions, cancellationToken);
}
public static async ValueTask<IAsyncReader> OpenAsyncReader(
@@ -89,7 +89,7 @@ public partial class TarReader
{
cancellationToken.ThrowIfCancellationRequested();
stream.NotNull(nameof(stream));
readerOptions ??= new ReaderOptions();
readerOptions ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(
stream,
bufferSize: Math.Max(
@@ -143,7 +143,7 @@ public partial class TarReader
CancellationToken cancellationToken = default
)
{
readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false };
readerOptions ??= ReaderOptions.ForFilePath;
var stream = fileInfo.OpenAsyncReadStream(cancellationToken);
return await OpenAsyncReader(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
@@ -158,7 +158,7 @@ public partial class TarReader
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= new ReaderOptions() { LeaveStreamOpen = false };
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
@@ -170,8 +170,8 @@ public partial class TarReader
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
readerOptions ??= new ReaderOptions();
stream.RequireReadable();
readerOptions ??= ReaderOptions.ForExternalStream;
var sharpCompressStream = SharpCompressStream.Create(
stream,
bufferSize: Math.Max(

View File

@@ -9,14 +9,14 @@ namespace SharpCompress.Readers.Zip;
public partial class ZipReader : IReaderOpenable
{
public static ValueTask<IAsyncReader> OpenAsyncReader(
string path,
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return new((IAsyncReader)OpenReader(new FileInfo(path), readerOptions));
filePath.NotNullOrEmpty(nameof(filePath));
return new((IAsyncReader)OpenReader(new FileInfo(filePath), readerOptions));
}
public static ValueTask<IAsyncReader> OpenAsyncReader(
@@ -48,6 +48,7 @@ public partial class ZipReader : IReaderOpenable
public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
readerOptions ??= ReaderOptions.ForFilePath;
return OpenReader(fileInfo.OpenRead(), readerOptions);
}
}

View File

@@ -47,8 +47,8 @@ public partial class ZipReader : AbstractReader<ZipEntry, ZipVolume>
/// <returns></returns>
public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
return new ZipReader(stream, readerOptions ?? new ReaderOptions());
stream.RequireReadable();
return new ZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream);
}
public static IReader OpenReader(
@@ -57,8 +57,8 @@ public partial class ZipReader : AbstractReader<ZipEntry, ZipVolume>
IEnumerable<ZipEntry> entries
)
{
stream.NotNull(nameof(stream));
return new ZipReader(stream, options ?? new ReaderOptions(), entries);
stream.RequireReadable();
return new ZipReader(stream, options ?? ReaderOptions.ForExternalStream, entries);
}
#endregion Open

View File

@@ -6,7 +6,7 @@
<AssemblyVersion>0.0.0.0</AssemblyVersion>
<FileVersion>0.0.0.0</FileVersion>
<Authors>Adam Hathcock</Authors>
<TargetFrameworks>net48;netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net10.0</TargetFrameworks>
<AssemblyName>SharpCompress</AssemblyName>
<AssemblyOriginatorKeyFile>../../SharpCompress.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SharpCompress;
internal static class StreamValidationExtensions
{
internal static void RequireReadable(this Stream stream)
{
stream.NotNull(nameof(stream));
if (!stream.CanRead)
{
throw new ArgumentException("Stream must be readable", nameof(stream));
}
}
internal static void RequireSeekable(this Stream stream)
{
stream.NotNull(nameof(stream));
if (!stream.CanSeek)
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
}
internal static void RequireWritable(this Stream stream)
{
stream.NotNull(nameof(stream));
if (!stream.CanWrite)
{
throw new ArgumentException("Stream must be writable", nameof(stream));
}
}
internal static IEnumerable<Stream> RequireSeekable(this IEnumerable<Stream> streams)
{
foreach (var stream in streams)
{
stream.RequireSeekable();
yield return stream;
}
}
internal static IEnumerable<Stream> RequireReadable(this IEnumerable<Stream> streams)
{
foreach (var stream in streams)
{
stream.RequireReadable();
yield return stream;
}
}
}

View File

@@ -60,6 +60,15 @@ internal static class ThrowHelper
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfGreaterThan(long value, long other, string? paramName = null)
{
if (value > other)
{
throw new ArgumentOutOfRangeException(paramName);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfGreaterThan(uint value, uint other, string? paramName = null)
{

View File

@@ -20,7 +20,7 @@ public abstract partial class AbstractWriter(ArchiveType type, IWriterOptions wr
protected Stream? OutputStream { get; private set; }
public ArchiveType WriterType { get; } = type;
public ArchiveType Type { get; } = type;
protected IWriterOptions WriterOptions { get; } = writerOptions;

View File

@@ -1,5 +1,7 @@
#if NET8_0_OR_GREATER
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Writers.GZip;
@@ -15,28 +17,43 @@ public partial class GZipWriter : IWriterOpenable<GZipWriterOptions>
public static IWriter OpenWriter(FileInfo fileInfo, GZipWriterOptions writerOptions)
{
fileInfo.NotNull(nameof(fileInfo));
return new GZipWriter(fileInfo.OpenWrite(), writerOptions);
return new GZipWriter(fileInfo.OpenWrite(), writerOptions with { LeaveStreamOpen = false });
}
public static IWriter OpenWriter(Stream stream, GZipWriterOptions writerOptions)
{
stream.NotNull(nameof(stream));
stream.RequireWritable();
return new GZipWriter(stream, writerOptions);
}
public static IAsyncWriter OpenAsyncWriter(string stream, GZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
string filePath,
GZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(filePath, writerOptions));
}
public static IAsyncWriter OpenAsyncWriter(Stream stream, GZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
Stream stream,
GZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(stream, writerOptions));
}
public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, GZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
FileInfo fileInfo,
GZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(fileInfo, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions));
}
}
#endif

View File

@@ -6,7 +6,7 @@ namespace SharpCompress.Writers;
public interface IWriter : IDisposable
{
ArchiveType WriterType { get; }
ArchiveType Type { get; }
void Write(string filename, Stream source, DateTime? modificationTime);
void WriteDirectory(string directoryName, DateTime? modificationTime);
}

View File

@@ -1,6 +1,7 @@
#if NET8_0_OR_GREATER
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Options;
namespace SharpCompress.Writers;
@@ -17,22 +18,25 @@ public interface IWriterOpenable<TWriterOptions>
/// Opens a Writer asynchronously.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="archiveType">The archive type.</param>
/// <param name="writerOptions">Writer options.</param>
/// <returns>A task that returns an IWriter.</returns>
public static abstract IAsyncWriter OpenAsyncWriter(
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that returns an async writer.</returns>
public static abstract ValueTask<IAsyncWriter> OpenAsyncWriter(
Stream stream,
TWriterOptions writerOptions
TWriterOptions writerOptions,
CancellationToken cancellationToken = default
);
public static abstract IAsyncWriter OpenAsyncWriter(
public static abstract ValueTask<IAsyncWriter> OpenAsyncWriter(
string filePath,
TWriterOptions writerOptions
TWriterOptions writerOptions,
CancellationToken cancellationToken = default
);
public static abstract IAsyncWriter OpenAsyncWriter(
public static abstract ValueTask<IAsyncWriter> OpenAsyncWriter(
FileInfo fileInfo,
TWriterOptions writerOptions
TWriterOptions writerOptions,
CancellationToken cancellationToken = default
);
}
#endif

View File

@@ -1,5 +1,7 @@
#if NET8_0_OR_GREATER
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Writers.SevenZip;
@@ -20,7 +22,13 @@ public partial class SevenZipWriter : IWriterOpenable<SevenZipWriterOptions>
public static IWriter OpenWriter(FileInfo fileInfo, SevenZipWriterOptions writerOptions)
{
fileInfo.NotNull(nameof(fileInfo));
return new SevenZipWriter(fileInfo.OpenWrite(), writerOptions);
return new SevenZipWriter(
fileInfo.OpenWrite(),
writerOptions with
{
LeaveStreamOpen = false,
}
);
}
/// <summary>
@@ -28,35 +36,47 @@ public partial class SevenZipWriter : IWriterOpenable<SevenZipWriterOptions>
/// </summary>
public static IWriter OpenWriter(Stream stream, SevenZipWriterOptions writerOptions)
{
stream.NotNull(nameof(stream));
stream.RequireWritable();
return new SevenZipWriter(stream, writerOptions);
}
/// <summary>
/// Opens a new async SevenZipWriter for the specified file path.
/// </summary>
public static IAsyncWriter OpenAsyncWriter(string filePath, SevenZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
string filePath,
SevenZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(filePath, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(filePath, writerOptions));
}
/// <summary>
/// Opens a new async SevenZipWriter for the specified stream.
/// </summary>
public static IAsyncWriter OpenAsyncWriter(Stream stream, SevenZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
Stream stream,
SevenZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(stream, writerOptions));
}
/// <summary>
/// Opens a new async SevenZipWriter for the specified file.
/// </summary>
public static IAsyncWriter OpenAsyncWriter(
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
FileInfo fileInfo,
SevenZipWriterOptions writerOptions
SevenZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(fileInfo, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions));
}
}
#endif

View File

@@ -203,7 +203,7 @@ public partial class SevenZipWriter : AbstractWriter
var filesInfo = new SevenZipFilesInfoWriter { Entries = entries.ToArray() };
// Write header to a temporary stream first
using var headerStream = new MemoryStream();
using var headerStream = new PooledMemoryStream();
ArchiveHeaderWriter.WriteRawHeader(headerStream, mainStreamsInfo, filesInfo);
// Optionally compress the header
@@ -244,7 +244,7 @@ public partial class SevenZipWriter : AbstractWriter
};
// Write encoded header to a second temporary stream
using var encodedHeaderStream = new MemoryStream();
using var encodedHeaderStream = new PooledMemoryStream();
ArchiveHeaderWriter.WriteEncodedHeader(encodedHeaderStream, headerStreamsInfo);
// Write the encoded header to the output
@@ -252,12 +252,10 @@ public partial class SevenZipWriter : AbstractWriter
encodedHeaderStream.Position = 0;
encodedHeaderStream.CopyTo(output);
// Compute CRC of the encoded header
var headerCrc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
encodedHeaderStream.GetBuffer().AsSpan(0, (int)encodedHeaderStream.Length)
);
// Compute CRC of the encoded header without allocating a contiguous buffer
var encodedHeaderCrcSink = new Crc32Stream(Stream.Null);
encodedHeaderStream.WriteTo(encodedHeaderCrcSink);
var headerCrc = encodedHeaderCrcSink.Crc;
// Back-patch signature header
var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize);
@@ -283,12 +281,10 @@ public partial class SevenZipWriter : AbstractWriter
rawHeaderStream.Position = 0;
rawHeaderStream.CopyTo(output);
// Compute CRC of the raw header
var headerCrc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
rawHeaderStream.GetBuffer().AsSpan(0, (int)rawHeaderStream.Length)
);
// Compute CRC of the raw header without allocating a contiguous buffer
var rawHeaderCrcSink = new Crc32Stream(Stream.Null);
rawHeaderStream.WriteTo(rawHeaderCrcSink);
var headerCrc = rawHeaderCrcSink.Crc;
// Back-patch signature header
var nextHeaderOffset = (ulong)(headerStartPos - SevenZipSignatureHeaderWriter.HeaderSize);

View File

@@ -1,5 +1,7 @@
#if NET8_0_OR_GREATER
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Writers.Tar;
@@ -15,28 +17,43 @@ public partial class TarWriter : IWriterOpenable<TarWriterOptions>
public static IWriter OpenWriter(FileInfo fileInfo, TarWriterOptions writerOptions)
{
fileInfo.NotNull(nameof(fileInfo));
return new TarWriter(fileInfo.OpenWrite(), writerOptions);
return new TarWriter(fileInfo.OpenWrite(), writerOptions with { LeaveStreamOpen = false });
}
public static IWriter OpenWriter(Stream stream, TarWriterOptions writerOptions)
{
stream.NotNull(nameof(stream));
stream.RequireWritable();
return new TarWriter(stream, writerOptions);
}
public static IAsyncWriter OpenAsyncWriter(string stream, TarWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
string filePath,
TarWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(filePath, writerOptions));
}
public static IAsyncWriter OpenAsyncWriter(Stream stream, TarWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
Stream stream,
TarWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(stream, writerOptions));
}
public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, TarWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
FileInfo fileInfo,
TarWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(fileInfo, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions));
}
}
#endif

View File

@@ -75,6 +75,8 @@ public static class WriterFactory
IWriterOptions writerOptions
)
{
stream.RequireWritable();
var factory = Factories
.Factory.Factories.OfType<IWriterFactory>()
.FirstOrDefault(item => item.KnownArchiveType == archiveType);
@@ -102,6 +104,8 @@ public static class WriterFactory
CancellationToken cancellationToken = default
)
{
stream.RequireWritable();
var factory = Factories
.Factory.Factories.OfType<IWriterFactory>()
.FirstOrDefault(item => item.KnownArchiveType == archiveType);

View File

@@ -1,7 +1,6 @@
using System;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.Compressors;
using SharpCompress.Providers;
using D = SharpCompress.Compressors.Deflate;
@@ -18,17 +17,10 @@ namespace SharpCompress.Writers;
/// </remarks>
public sealed record WriterOptions : IWriterOptions
{
private CompressionType _compressionType;
private int _compressionLevel;
/// <summary>
/// The compression type to use for the archive.
/// </summary>
public CompressionType CompressionType
{
get => _compressionType;
init => _compressionType = value;
}
public CompressionType CompressionType { get; init; }
/// <summary>
/// The compression level to be used when the compression type supports variable levels.
@@ -40,11 +32,11 @@ public sealed record WriterOptions : IWriterOptions
/// </summary>
public int CompressionLevel
{
get => _compressionLevel;
get;
init
{
CompressionLevelValidation.Validate(CompressionType, value);
_compressionLevel = value;
field = value;
}
}

View File

@@ -1,5 +1,7 @@
#if NET8_0_OR_GREATER
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Writers.Zip;
@@ -15,28 +17,43 @@ public partial class ZipWriter : IWriterOpenable<ZipWriterOptions>
public static IWriter OpenWriter(FileInfo fileInfo, ZipWriterOptions writerOptions)
{
fileInfo.NotNull(nameof(fileInfo));
return new ZipWriter(fileInfo.OpenWrite(), writerOptions);
return new ZipWriter(fileInfo.OpenWrite(), writerOptions with { LeaveStreamOpen = false });
}
public static IWriter OpenWriter(Stream stream, ZipWriterOptions writerOptions)
{
stream.NotNull(nameof(stream));
stream.RequireWritable();
return new ZipWriter(stream, writerOptions);
}
public static IAsyncWriter OpenAsyncWriter(string stream, ZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
string filePath,
ZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(filePath, writerOptions));
}
public static IAsyncWriter OpenAsyncWriter(Stream stream, ZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
Stream stream,
ZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(stream, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(stream, writerOptions));
}
public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, ZipWriterOptions writerOptions)
public static ValueTask<IAsyncWriter> OpenAsyncWriter(
FileInfo fileInfo,
ZipWriterOptions writerOptions,
CancellationToken cancellationToken = default
)
{
return (IAsyncWriter)OpenWriter(fileInfo, writerOptions);
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncWriter)OpenWriter(fileInfo, writerOptions));
}
}
#endif

View File

@@ -313,48 +313,6 @@
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
}
},
".NETCoreApp,Version=v5.0": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
"resolved": "1.0.3",
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[10.0.102, )",
"resolved": "10.0.102",
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "10.0.102",
"Microsoft.SourceLink.Common": "10.0.102"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Direct",
"requested": "[17.14.15, )",
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
}
},
"net6.0": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
@@ -397,48 +355,6 @@
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
}
},
"net7.0": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
"resolved": "1.0.3",
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[10.0.102, )",
"resolved": "10.0.102",
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "10.0.102",
"Microsoft.SourceLink.Common": "10.0.102"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Direct",
"requested": "[17.14.15, )",
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
}
},
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
@@ -486,48 +402,6 @@
"resolved": "10.0.102",
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
}
},
"net9.0": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
"resolved": "1.0.3",
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[10.0.102, )",
"resolved": "10.0.102",
"contentHash": "Oxq3RCIJSdtpIU4hLqO7XaDe/Ra3HS9Wi8rJl838SAg6Zu1iQjerA0+xXWBgUFYbgknUGCLOU0T+lzMLkvY9Qg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "10.0.102",
"Microsoft.SourceLink.Common": "10.0.102"
}
},
"Microsoft.VisualStudio.Threading.Analyzers": {
"type": "Direct",
"requested": "[17.14.15, )",
"resolved": "17.14.15",
"contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg=="
},
"Microsoft.NETFramework.ReferenceAssemblies.net461": {
"type": "Transitive",
"resolved": "1.0.3",
"contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "10.0.102",
"contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A=="
}
}
}
}

View File

@@ -70,7 +70,7 @@ public class TarBenchmarks : ArchiveBenchmarkBase
using var stream = new MemoryStream(_tarBytes);
using var archive = TarArchive.OpenArchive(
stream,
new ReaderOptions().WithProviders(
ReaderOptions.ForExternalStream.WithProviders(
CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider())
)
);
@@ -87,7 +87,7 @@ public class TarBenchmarks : ArchiveBenchmarkBase
using var stream = new MemoryStream(_tarBytes);
using var reader = ReaderFactory.OpenReader(
stream,
new ReaderOptions().WithProviders(
ReaderOptions.ForExternalStream.WithProviders(
CompressionProviderRegistry.Empty.With(new SystemGZipCompressionProvider())
)
);

View File

@@ -32,7 +32,7 @@ public class ZipBenchmarks : ArchiveBenchmarkBase
using var stream = new MemoryStream(_archiveBytes);
using var archive = ZipArchive.OpenArchive(
stream,
new ReaderOptions().WithProviders(
ReaderOptions.ForExternalStream.WithProviders(
CompressionProviderRegistry.Empty.With(new SystemDeflateCompressionProvider())
)
);
@@ -73,7 +73,7 @@ public class ZipBenchmarks : ArchiveBenchmarkBase
using var stream = new MemoryStream(_archiveBytes);
using var reader = ReaderFactory.OpenReader(
stream,
new ReaderOptions().WithProviders(
ReaderOptions.ForExternalStream.WithProviders(
CompressionProviderRegistry.Empty.With(new SystemDeflateCompressionProvider())
)
);

Some files were not shown because too many files have changed in this diff Show More