Compare commits
	
		
			14 commits
		
	
	
		
			
				2d6ba5ac10
			
			...
			
				a604e4472d
			
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							
							
								
							
							
	
	
		
			
		
	
	a604e4472d | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	071a87bca4 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	170214eec9 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	8384466325 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	a8e33b416f | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	996da146e1 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	cce3ed4671 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	e1e51789d8 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	abfcb66d22 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	9201451c36 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	7915e1ea07 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	684b69399c | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	550045c380 | 
						
						
							|||
| 
							
							
								
							
							
	
	
		
			
		
	
	7dee79b6a0 | 
						
						
							
					 113 changed files with 13652 additions and 105 deletions
				
			
		
							
								
								
									
										6
									
								
								.env.example
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.env.example
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					CACHE_DIR=/tmp/artifactview
 | 
				
			||||||
 | 
					MAX_ARTIFACT_SIZE=100000000
 | 
				
			||||||
 | 
					MAX_AGE_H=12
 | 
				
			||||||
 | 
					# If you only want to access public repositories,
 | 
				
			||||||
 | 
					# create a fine-grained token with Public Repositories (read-only) access
 | 
				
			||||||
 | 
					GITHUB_TOKEN=github_pat_123456
 | 
				
			||||||
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1 +1,2 @@
 | 
				
			||||||
/target
 | 
					/target
 | 
				
			||||||
 | 
					/.env
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								.pre-commit-config.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.pre-commit-config.yaml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					repos:
 | 
				
			||||||
 | 
					  - repo: https://github.com/pre-commit/pre-commit-hooks
 | 
				
			||||||
 | 
					    rev: v4.3.0
 | 
				
			||||||
 | 
					    hooks:
 | 
				
			||||||
 | 
					      - id: end-of-file-fixer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - repo: https://github.com/cathiele/pre-commit-rust
 | 
				
			||||||
 | 
					    rev: v0.1.0
 | 
				
			||||||
 | 
					    hooks:
 | 
				
			||||||
 | 
					      - id: cargo-fmt
 | 
				
			||||||
 | 
					      - id: cargo-clippy
 | 
				
			||||||
 | 
					        args: ["--all", "--tests", "--", "-D", "warnings"]
 | 
				
			||||||
							
								
								
									
										1333
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1333
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										38
									
								
								Cargo.toml
									
										
									
									
									
								
							
							
						
						
									
										38
									
								
								Cargo.toml
									
										
									
									
									
								
							| 
						 | 
					@ -4,15 +4,47 @@ version = "0.1.0"
 | 
				
			||||||
edition = "2021"
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
anyhow = "1.0.86"
 | 
					async_zip = { path = "crates/async_zip", features = ["tokio", "tokio-fs", "deflate"] }
 | 
				
			||||||
arc-swap = "1.7.1"
 | 
					axum = { version = "0.7.5", features = ["http2"] }
 | 
				
			||||||
 | 
					axum-extra = { version = "0.9.3", features = ["typed-header"] }
 | 
				
			||||||
 | 
					dotenvy = "0.15.7"
 | 
				
			||||||
 | 
					envy = { path = "crates/envy" }
 | 
				
			||||||
 | 
					flate2 = "1.0.30"
 | 
				
			||||||
 | 
					futures-lite = "2.3.0"
 | 
				
			||||||
 | 
					headers = "0.4.0"
 | 
				
			||||||
 | 
					hex = "0.4.3"
 | 
				
			||||||
 | 
					http = "1.1.0"
 | 
				
			||||||
 | 
					mime = "0.3.17"
 | 
				
			||||||
 | 
					mime_guess = "2.0.4"
 | 
				
			||||||
once_cell = "1.19.0"
 | 
					once_cell = "1.19.0"
 | 
				
			||||||
 | 
					path_macro = "1.0.0"
 | 
				
			||||||
 | 
					percent-encoding = "2.3.1"
 | 
				
			||||||
 | 
					pin-project = "1.1.5"
 | 
				
			||||||
 | 
					quick_cache = "0.5.1"
 | 
				
			||||||
 | 
					rand = "0.8.5"
 | 
				
			||||||
regex = "1.10.4"
 | 
					regex = "1.10.4"
 | 
				
			||||||
reqwest = { version = "0.12.4", features = ["json"] }
 | 
					reqwest = { version = "0.12.4", features = ["json"] }
 | 
				
			||||||
serde = { version = "1.0.203", features = ["derive"] }
 | 
					serde = { version = "1.0.203", features = ["derive"] }
 | 
				
			||||||
 | 
					serde-env = "0.1.1"
 | 
				
			||||||
 | 
					serde-hex = "0.1.0"
 | 
				
			||||||
serde_json = "1.0.117"
 | 
					serde_json = "1.0.117"
 | 
				
			||||||
tokio = {version = "1.37.0", features = ["macros"]}
 | 
					siphasher = "1.0.1"
 | 
				
			||||||
 | 
					thiserror = "1.0.61"
 | 
				
			||||||
 | 
					tokio = { version = "1.37.0", features = ["macros", "fs", "rt-multi-thread"] }
 | 
				
			||||||
 | 
					tokio-util = { version = "0.7.11", features = ["io"] }
 | 
				
			||||||
 | 
					tower-http = { version = "0.5.2", features = ["trace"] }
 | 
				
			||||||
 | 
					tracing = "0.1.40"
 | 
				
			||||||
 | 
					tracing-subscriber = "0.3.18"
 | 
				
			||||||
 | 
					url = "2.5.0"
 | 
				
			||||||
 | 
					yarte = "0.15.7"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[build-dependencies]
 | 
				
			||||||
 | 
					yarte_helpers = "0.15.8"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dev-dependencies]
 | 
					[dev-dependencies]
 | 
				
			||||||
proptest = "1.4.0"
 | 
					proptest = "1.4.0"
 | 
				
			||||||
rstest = { version = "0.19.0", default-features = false }
 | 
					rstest = { version = "0.19.0", default-features = false }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[workspace]
 | 
				
			||||||
 | 
					members = [".", "crates/*"]
 | 
				
			||||||
 | 
					resolver = "2"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								Justfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Justfile
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					test:
 | 
				
			||||||
 | 
					    cargo test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					release:
 | 
				
			||||||
 | 
					    #!/usr/bin/env bash
 | 
				
			||||||
 | 
					    set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CRATE="artifactview"
 | 
				
			||||||
 | 
					    CHANGELOG="CHANGELOG.md"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    VERSION=$(cargo pkgid --package "$CRATE" | tr '#@' '\n' | tail -n 1)
 | 
				
			||||||
 | 
					    TAG="v${VERSION}"
 | 
				
			||||||
 | 
					    echo "Releasing $TAG:"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if git rev-parse "$TAG" >/dev/null 2>&1; then echo "version tag $TAG already exists"; exit 1; fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CLIFF_ARGS="--tag '${TAG}' --unreleased"
 | 
				
			||||||
 | 
					    echo "git-cliff $CLIFF_ARGS"
 | 
				
			||||||
 | 
					    if [ -f "$CHANGELOG" ]; then
 | 
				
			||||||
 | 
					        eval "git-cliff $CLIFF_ARGS --prepend '$CHANGELOG'"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					        eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    git add "$CHANGELOG"
 | 
				
			||||||
 | 
					    git commit -m "chore(release): release $CRATE v$VERSION"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    echo "🚀 Run 'git push origin $TAG' to publish"
 | 
				
			||||||
							
								
								
									
										21
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
# artifactview
 | 
					# Artifactview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
View CI build artifacts from Forgejo/Github using your web browser.
 | 
					View CI build artifacts from Forgejo/Github using your web browser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,4 +20,21 @@ status code 404 if no file was found.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Artifactview accepts URLs in the given format: `<HOST>--<USER>--<REPO>--<RUN>-<ARTIFACT>.example.com`
 | 
					Artifactview accepts URLs in the given format: `<HOST>--<USER>--<REPO>--<RUN>-<ARTIFACT>.example.com`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Example: `github-com--theta-dev--example-project--4-11.example.com`
 | 
					Example: `https://github-com--theta-dev--example-project--4-11.example.com`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Security considerations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It is recommended to use the whitelist feature to limit Artifactview to access only trusted
 | 
				
			||||||
 | 
					servers, users and organizations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Since many
 | 
				
			||||||
 | 
					[well-known URIs](https://www.iana.org/assignments/well-known-uris/well-known-uris.xhtml)
 | 
				
			||||||
 | 
					are used to configure security-relevant properties of a website or are used to attest
 | 
				
			||||||
 | 
					ownership of a website (like `.well-known/acme-challenge` for issuing TLS certificates),
 | 
				
			||||||
 | 
					Artifactview will serve no files from the `.well-known` folder.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					There is a configurable limit for both the maximum downloaded artifact size and the
 | 
				
			||||||
 | 
					maximum size of individual files to be served (100MB by default).
 | 
				
			||||||
 | 
					Additionally there is a configurable timeout for the zip file indexing operation.
 | 
				
			||||||
 | 
					These measures should protect the server againt denial-of-service attacks like
 | 
				
			||||||
 | 
					overfilling the server drive or uploading zip bombs.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								build.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								build.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					fn main() {
 | 
				
			||||||
 | 
					    yarte_helpers::recompile::when_changed();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								crates/async_zip/.cargo-ok
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								crates/async_zip/.cargo-ok
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					{"v":1}
 | 
				
			||||||
							
								
								
									
										6
									
								
								crates/async_zip/.cargo_vcs_info.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								crates/async_zip/.cargo_vcs_info.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "git": {
 | 
				
			||||||
 | 
					    "sha1": "e4ee7a521f624aea3c2c3eef6b78fb1ec057504b"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "path_in_vcs": ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								crates/async_zip/.github/dependabot.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								crates/async_zip/.github/dependabot.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					version: 2
 | 
				
			||||||
 | 
					updates:
 | 
				
			||||||
 | 
					  - package-ecosystem: "github-actions"
 | 
				
			||||||
 | 
					    # Workflow files stored in the
 | 
				
			||||||
 | 
					    # default location of `.github/workflows`
 | 
				
			||||||
 | 
					    directory: "/"
 | 
				
			||||||
 | 
					    schedule:
 | 
				
			||||||
 | 
					      interval: "daily"
 | 
				
			||||||
 | 
					  - package-ecosystem: "cargo"
 | 
				
			||||||
 | 
					    directory: "/"
 | 
				
			||||||
 | 
					    schedule:
 | 
				
			||||||
 | 
					      interval: "daily"
 | 
				
			||||||
							
								
								
									
										20
									
								
								crates/async_zip/.github/workflows/ci-clippy.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								crates/async_zip/.github/workflows/ci-clippy.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					name: clippy (Linux)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  CARGO_TERM_COLOR: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					    - name: Run clippy
 | 
				
			||||||
 | 
					      run: cargo clippy --all-features -- -D clippy::all
 | 
				
			||||||
							
								
								
									
										20
									
								
								crates/async_zip/.github/workflows/ci-fmt.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								crates/async_zip/.github/workflows/ci-fmt.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					name: rustfmt (Linux)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  CARGO_TERM_COLOR: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					    - name: Run rustfmt
 | 
				
			||||||
 | 
					      run: cargo fmt --check
 | 
				
			||||||
							
								
								
									
										51
									
								
								crates/async_zip/.github/workflows/ci-linux.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								crates/async_zip/.github/workflows/ci-linux.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					name: Test (Linux)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  CARGO_TERM_COLOR: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test [no features]
 | 
				
			||||||
 | 
					      run: cargo test --verbose
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['chrono' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features chrono
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['tokio' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features tokio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['tokio-fs' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features tokio-fs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['deflate' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features deflate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['bzip2' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features bzip2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['lzma' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features lzma
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['zstd' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features zstd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['xz' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features xz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['deflate64' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features deflate64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Test ['full' feature]
 | 
				
			||||||
 | 
					      run: cargo test --verbose --features full
 | 
				
			||||||
							
								
								
									
										24
									
								
								crates/async_zip/.github/workflows/ci-typos.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								crates/async_zip/.github/workflows/ci-typos.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					name: typos (Linux)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  CARGO_TERM_COLOR: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Install typos
 | 
				
			||||||
 | 
					      run: cargo install typos-cli
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Run typos
 | 
				
			||||||
 | 
					      run: typos --format brief
 | 
				
			||||||
							
								
								
									
										24
									
								
								crates/async_zip/.github/workflows/ci-wasm.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								crates/async_zip/.github/workflows/ci-wasm.yml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					name: Build (WASM)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  CARGO_TERM_COLOR: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					    name: Build ['full-wasm' feature] on ${{ matrix.target }}
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        target:
 | 
				
			||||||
 | 
					          - wasm32-wasi
 | 
				
			||||||
 | 
					          - wasm32-unknown-unknown
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					    - run: rustup target add ${{ matrix.target }}
 | 
				
			||||||
 | 
					    - run: cargo build --verbose --target ${{ matrix.target }} --features full-wasm
 | 
				
			||||||
							
								
								
									
										15
									
								
								crates/async_zip/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								crates/async_zip/.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					# Generated by Cargo
 | 
				
			||||||
 | 
					# will have compiled files and executables
 | 
				
			||||||
 | 
					/target/
 | 
				
			||||||
 | 
					/examples/**/target/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
 | 
				
			||||||
 | 
					# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
 | 
				
			||||||
 | 
					/Cargo.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# These are backup files generated by rustfmt
 | 
				
			||||||
 | 
					**/*.rs.bk
 | 
				
			||||||
 | 
					/examples/**/*.rs.bk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ignore generated zip test file that is large
 | 
				
			||||||
 | 
					/src/tests/read/zip64/zip64many.zip
 | 
				
			||||||
							
								
								
									
										63
									
								
								crates/async_zip/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								crates/async_zip/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,63 @@
 | 
				
			||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "async_zip"
 | 
				
			||||||
 | 
					version = "0.0.17"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					authors = ["Harry [hello@majored.pw]"]
 | 
				
			||||||
 | 
					repository = "https://github.com/Majored/rs-async-zip"
 | 
				
			||||||
 | 
					description = "An asynchronous ZIP archive reading/writing crate."
 | 
				
			||||||
 | 
					readme = "README.md"
 | 
				
			||||||
 | 
					license = "MIT"
 | 
				
			||||||
 | 
					documentation = "https://docs.rs/async_zip/"
 | 
				
			||||||
 | 
					homepage = "https://github.com/Majored/rs-async-zip"
 | 
				
			||||||
 | 
					keywords = ["async", "zip", "archive", "tokio"]
 | 
				
			||||||
 | 
					categories = ["asynchronous", "compression"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[features]
 | 
				
			||||||
 | 
					full = ["chrono", "tokio-fs", "deflate", "bzip2", "lzma", "zstd", "xz", "deflate64"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# All features that are compatible with WASM
 | 
				
			||||||
 | 
					full-wasm = ["chrono", "deflate", "zstd"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tokio = ["dep:tokio", "tokio-util", "tokio/io-util"]
 | 
				
			||||||
 | 
					tokio-fs = ["tokio/fs"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					deflate = ["async-compression/deflate"]
 | 
				
			||||||
 | 
					bzip2 = ["async-compression/bzip2"]
 | 
				
			||||||
 | 
					lzma = ["async-compression/lzma"]
 | 
				
			||||||
 | 
					zstd = ["async-compression/zstd"]
 | 
				
			||||||
 | 
					xz = ["async-compression/xz"]
 | 
				
			||||||
 | 
					deflate64 = ["async-compression/deflate64"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.metadata.docs.rs]
 | 
				
			||||||
 | 
					all-features = true
 | 
				
			||||||
 | 
					# defines the configuration attribute `docsrs`
 | 
				
			||||||
 | 
					rustdoc-args = ["--cfg", "docsrs"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					crc32fast = "1"
 | 
				
			||||||
 | 
					futures-lite = { version = "2.1.0", default-features = false, features = ["std"] }
 | 
				
			||||||
 | 
					pin-project = "1"
 | 
				
			||||||
 | 
					thiserror = "1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async-compression = { version = "0.4.2", default-features = false, features = ["futures-io"], optional = true }
 | 
				
			||||||
 | 
					chrono = { version = "0.4", default-features = false, features = ["clock"], optional = true }
 | 
				
			||||||
 | 
					tokio = { version = "1", default-features = false, optional = true }
 | 
				
			||||||
 | 
					tokio-util = { version = "0.7", features = ["compat"], optional = true }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dev-dependencies]
 | 
				
			||||||
 | 
					# tests
 | 
				
			||||||
 | 
					tokio = { version = "1", features = ["full"] }
 | 
				
			||||||
 | 
					tokio-util = { version = "0.7", features = ["compat"] }
 | 
				
			||||||
 | 
					env_logger = "0.11.2"
 | 
				
			||||||
 | 
					zip = "0.6.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# shared across multiple examples
 | 
				
			||||||
 | 
					# anyhow = "1"
 | 
				
			||||||
 | 
					# sanitize-filename = "0.5"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# actix_multipart
 | 
				
			||||||
 | 
					# actix-web = "4"
 | 
				
			||||||
 | 
					# actix-multipart = "0.6"
 | 
				
			||||||
 | 
					# futures = "0.3"
 | 
				
			||||||
 | 
					# derive_more = "0.99"
 | 
				
			||||||
 | 
					# uuid = { version = "1", features = ["v4", "serde"] }
 | 
				
			||||||
							
								
								
									
										22
									
								
								crates/async_zip/LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								crates/async_zip/LICENSE
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					MIT License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Copyright (c) 2021 Harry
 | 
				
			||||||
 | 
					Copyright (c) 2023 Cognite AS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
 | 
					of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 | 
					in the Software without restriction, including without limitation the rights
 | 
				
			||||||
 | 
					to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
				
			||||||
 | 
					copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
 | 
					furnished to do so, subject to the following conditions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
				
			||||||
 | 
					FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
				
			||||||
 | 
					AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
				
			||||||
 | 
					LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
				
			||||||
 | 
					SOFTWARE.
 | 
				
			||||||
							
								
								
									
										81
									
								
								crates/async_zip/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								crates/async_zip/README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,81 @@
 | 
				
			||||||
 | 
					# async_zip
 | 
				
			||||||
 | 
					[](https://crates.io/crates/async_zip)
 | 
				
			||||||
 | 
					[](https://crates.io/crates/async_zip)
 | 
				
			||||||
 | 
					[](https://docs.rs/async_zip/)
 | 
				
			||||||
 | 
					[](https://github.com/Majored/rs-async-zip/actions?query=branch%3Amain)
 | 
				
			||||||
 | 
					[](https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An asynchronous ZIP archive reading/writing crate.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Features
 | 
				
			||||||
 | 
					- A base implementation atop `futures`'s IO traits.
 | 
				
			||||||
 | 
					- An extended implementation atop `tokio`'s IO traits.
 | 
				
			||||||
 | 
					- Support for Stored, Deflate, bzip2, LZMA, zstd, and xz compression methods.
 | 
				
			||||||
 | 
					- Various different reading approaches (seek, stream, filesystem, in-memory buffer, etc).
 | 
				
			||||||
 | 
					- Support for writing complete data (u8 slices) or streams using data descriptors.
 | 
				
			||||||
 | 
					- Initial support for ZIP64 reading and writing.
 | 
				
			||||||
 | 
					- Aims for reasonable [specification](https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md) compliance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Installation & Basic Usage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```toml
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					async_zip = { version = "0.0.17", features = ["full"] }
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A (soon to be) extensive list of [examples](https://github.com/Majored/rs-async-zip/tree/main/examples) can be found under the `/examples` directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Feature Flags
 | 
				
			||||||
 | 
					- `full` - Enables all below features.
 | 
				
			||||||
 | 
					- `full-wasm` - Enables all below features that are compatible with WASM.
 | 
				
			||||||
 | 
					- `chrono` - Enables support for parsing dates via `chrono`.
 | 
				
			||||||
 | 
					- `tokio` - Enables support for the `tokio` implementation module.
 | 
				
			||||||
 | 
					- `tokio-fs` - Enables support for the `tokio::fs` reading module.
 | 
				
			||||||
 | 
					- `deflate` - Enables support for the Deflate compression method.
 | 
				
			||||||
 | 
					- `bzip2` - Enables support for the bzip2 compression method.
 | 
				
			||||||
 | 
					- `lzma` - Enables support for the LZMA compression method.
 | 
				
			||||||
 | 
					- `zstd` - Enables support for the zstd compression method.
 | 
				
			||||||
 | 
					- `xz` - Enables support for the xz compression method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Reading
 | 
				
			||||||
 | 
					```rust
 | 
				
			||||||
 | 
					use tokio::{io::BufReader, fs::File};
 | 
				
			||||||
 | 
					use async_zip::tokio::read::seek::ZipFileReader;
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let mut file = BufReader::new(File::open("./Archive.zip").await?);
 | 
				
			||||||
 | 
					let mut zip = ZipFileReader::with_tokio(&mut file).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let mut string = String::new();
 | 
				
			||||||
 | 
					let mut reader = zip.reader_with_entry(0).await?;
 | 
				
			||||||
 | 
					reader.read_to_string_checked(&mut string).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					println!("{}", string);
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Writing
 | 
				
			||||||
 | 
					```rust
 | 
				
			||||||
 | 
					use async_zip::tokio::write::ZipFileWriter;
 | 
				
			||||||
 | 
					use async_zip::{Compression, ZipEntryBuilder};
 | 
				
			||||||
 | 
					use tokio::fs::File;
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let mut file = File::create("foo.zip").await?;
 | 
				
			||||||
 | 
					let mut writer = ZipFileWriter::with_tokio(&mut file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let data = b"This is an example file.";
 | 
				
			||||||
 | 
					let builder = ZipEntryBuilder::new("bar.txt".into(), Compression::Deflate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					writer.write_entry_whole(builder, data).await?;
 | 
				
			||||||
 | 
					writer.close().await?;
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Contributions
 | 
				
			||||||
 | 
					Whilst I will be continuing to maintain this crate myself, reasonable specification compliance is a huge undertaking for a single individual. As such, contributions will always be encouraged and appreciated.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					No contribution guidelines exist but additions should be developed with readability in mind, with appropriate comments, and make use of `rustfmt`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Issues & Support
 | 
				
			||||||
 | 
					Whether you're wanting to report a bug you've come across during use of this crate or are seeking general help/assistance, please utilise the [issues tracker](https://github.com/Majored/rs-async-zip/issues) and provide as much detail as possible (eg. recreation steps).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					I try to respond to issues within a reasonable timeframe.
 | 
				
			||||||
							
								
								
									
										3996
									
								
								crates/async_zip/SPECIFICATION.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3996
									
								
								crates/async_zip/SPECIFICATION.md
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										2
									
								
								crates/async_zip/rustfmt.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								crates/async_zip/rustfmt.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					max_width = 120
 | 
				
			||||||
 | 
					use_small_heuristics = "Max"
 | 
				
			||||||
							
								
								
									
										7
									
								
								crates/async_zip/src/base/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								crates/async_zip/src/base/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A base runtime-agnostic implementation using `futures`'s IO types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod read;
 | 
				
			||||||
 | 
					pub mod write;
 | 
				
			||||||
							
								
								
									
										68
									
								
								crates/async_zip/src/base/read/io/combined_record.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								crates/async_zip/src/base/read/io/combined_record.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Cognite AS
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::spec::header::{EndOfCentralDirectoryHeader, Zip64EndOfCentralDirectoryRecord};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Combines all the fields in EOCDR and Zip64EOCDR into one struct.
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct CombinedCentralDirectoryRecord {
 | 
				
			||||||
 | 
					    pub version_made_by: Option<u16>,
 | 
				
			||||||
 | 
					    pub version_needed_to_extract: Option<u16>,
 | 
				
			||||||
 | 
					    pub disk_number: u32,
 | 
				
			||||||
 | 
					    pub disk_number_start_of_cd: u32,
 | 
				
			||||||
 | 
					    pub num_entries_in_directory_on_disk: u64,
 | 
				
			||||||
 | 
					    pub num_entries_in_directory: u64,
 | 
				
			||||||
 | 
					    pub directory_size: u64,
 | 
				
			||||||
 | 
					    pub offset_of_start_of_directory: u64,
 | 
				
			||||||
 | 
					    pub file_comment_length: u16,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl CombinedCentralDirectoryRecord {
 | 
				
			||||||
 | 
					    /// Combine an EOCDR with an optional Zip64EOCDR.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Fields that are set to their max value in the EOCDR will be overwritten by the contents of
 | 
				
			||||||
 | 
					    /// the corresponding Zip64EOCDR field.
 | 
				
			||||||
 | 
					    pub fn combine(eocdr: EndOfCentralDirectoryHeader, zip64eocdr: Zip64EndOfCentralDirectoryRecord) -> Self {
 | 
				
			||||||
 | 
					        let mut combined = Self::from(&eocdr);
 | 
				
			||||||
 | 
					        if eocdr.disk_num == u16::MAX {
 | 
				
			||||||
 | 
					            combined.disk_number = zip64eocdr.disk_number;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if eocdr.start_cent_dir_disk == u16::MAX {
 | 
				
			||||||
 | 
					            combined.disk_number_start_of_cd = zip64eocdr.disk_number_start_of_cd;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if eocdr.num_of_entries_disk == u16::MAX {
 | 
				
			||||||
 | 
					            combined.num_entries_in_directory_on_disk = zip64eocdr.num_entries_in_directory_on_disk;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if eocdr.num_of_entries == u16::MAX {
 | 
				
			||||||
 | 
					            combined.num_entries_in_directory = zip64eocdr.num_entries_in_directory;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if eocdr.size_cent_dir == u32::MAX {
 | 
				
			||||||
 | 
					            combined.directory_size = zip64eocdr.directory_size;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if eocdr.cent_dir_offset == u32::MAX {
 | 
				
			||||||
 | 
					            combined.offset_of_start_of_directory = zip64eocdr.offset_of_start_of_directory;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        combined.version_made_by = Some(zip64eocdr.version_made_by);
 | 
				
			||||||
 | 
					        combined.version_needed_to_extract = Some(zip64eocdr.version_needed_to_extract);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        combined
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// An implementation for the case of no zip64EOCDR.
 | 
				
			||||||
 | 
					impl From<&EndOfCentralDirectoryHeader> for CombinedCentralDirectoryRecord {
 | 
				
			||||||
 | 
					    fn from(header: &EndOfCentralDirectoryHeader) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            version_made_by: None,
 | 
				
			||||||
 | 
					            version_needed_to_extract: None,
 | 
				
			||||||
 | 
					            disk_number: header.disk_num as u32,
 | 
				
			||||||
 | 
					            disk_number_start_of_cd: header.start_cent_dir_disk as u32,
 | 
				
			||||||
 | 
					            num_entries_in_directory_on_disk: header.num_of_entries_disk as u64,
 | 
				
			||||||
 | 
					            num_entries_in_directory: header.num_of_entries as u64,
 | 
				
			||||||
 | 
					            directory_size: header.size_cent_dir as u64,
 | 
				
			||||||
 | 
					            offset_of_start_of_directory: header.cent_dir_offset as u64,
 | 
				
			||||||
 | 
					            file_comment_length: header.file_comm_length,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										103
									
								
								crates/async_zip/src/base/read/io/compressed.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								crates/async_zip/src/base/read/io/compressed.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,103 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::spec::Compression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(any(
 | 
				
			||||||
 | 
					    feature = "deflate",
 | 
				
			||||||
 | 
					    feature = "bzip2",
 | 
				
			||||||
 | 
					    feature = "zstd",
 | 
				
			||||||
 | 
					    feature = "lzma",
 | 
				
			||||||
 | 
					    feature = "xz",
 | 
				
			||||||
 | 
					    feature = "deflate64"
 | 
				
			||||||
 | 
					))]
 | 
				
			||||||
 | 
					use async_compression::futures::bufread;
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncBufRead, AsyncRead};
 | 
				
			||||||
 | 
					use pin_project::pin_project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A wrapping reader which holds concrete types for all respective compression method readers.
 | 
				
			||||||
 | 
					#[pin_project(project = CompressedReaderProj)]
 | 
				
			||||||
 | 
					pub(crate) enum CompressedReader<R> {
 | 
				
			||||||
 | 
					    Stored(#[pin] R),
 | 
				
			||||||
 | 
					    #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					    Deflate(#[pin] bufread::DeflateDecoder<R>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					    Deflate64(#[pin] bufread::Deflate64Decoder<R>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					    Bz(#[pin] bufread::BzDecoder<R>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					    Lzma(#[pin] bufread::LzmaDecoder<R>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					    Zstd(#[pin] bufread::ZstdDecoder<R>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					    Xz(#[pin] bufread::XzDecoder<R>),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<R> CompressedReader<R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new wrapping reader from a generic [`AsyncBufRead`] implementer.
 | 
				
			||||||
 | 
					    pub(crate) fn new(reader: R, compression: Compression) -> Self {
 | 
				
			||||||
 | 
					        match compression {
 | 
				
			||||||
 | 
					            Compression::Stored => CompressedReader::Stored(reader),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            Compression::Deflate => CompressedReader::Deflate(bufread::DeflateDecoder::new(reader)),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					            Compression::Deflate64 => CompressedReader::Deflate64(bufread::Deflate64Decoder::new(reader)),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            Compression::Bz => CompressedReader::Bz(bufread::BzDecoder::new(reader)),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            Compression::Lzma => CompressedReader::Lzma(bufread::LzmaDecoder::new(reader)),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            Compression::Zstd => CompressedReader::Zstd(bufread::ZstdDecoder::new(reader)),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            Compression::Xz => CompressedReader::Xz(bufread::XzDecoder::new(reader)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this reader and returns the inner value.
 | 
				
			||||||
 | 
					    pub(crate) fn into_inner(self) -> R {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            CompressedReader::Stored(inner) => inner,
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            CompressedReader::Deflate(inner) => inner.into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					            CompressedReader::Deflate64(inner) => inner.into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            CompressedReader::Bz(inner) => inner.into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            CompressedReader::Lzma(inner) => inner.into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            CompressedReader::Zstd(inner) => inner.into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            CompressedReader::Xz(inner) => inner.into_inner(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<R> AsyncRead for CompressedReader<R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    fn poll_read(self: Pin<&mut Self>, c: &mut Context<'_>, b: &mut [u8]) -> Poll<std::io::Result<usize>> {
 | 
				
			||||||
 | 
					        match self.project() {
 | 
				
			||||||
 | 
					            CompressedReaderProj::Stored(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            CompressedReaderProj::Deflate(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					            CompressedReaderProj::Deflate64(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            CompressedReaderProj::Bz(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            CompressedReaderProj::Lzma(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            CompressedReaderProj::Zstd(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            CompressedReaderProj::Xz(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										128
									
								
								crates/async_zip/src/base/read/io/entry.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								crates/async_zip/src/base/read/io/entry.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,128 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::{compressed::CompressedReader, hashed::HashedReader, owned::OwnedReader};
 | 
				
			||||||
 | 
					use crate::entry::ZipEntry;
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::Compression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncBufRead, AsyncRead, AsyncReadExt, Take};
 | 
				
			||||||
 | 
					use pin_project::pin_project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type which encodes that [`ZipEntryReader`] has associated entry data.
 | 
				
			||||||
 | 
					pub struct WithEntry<'a>(OwnedEntry<'a>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type which encodes that [`ZipEntryReader`] has no associated entry data.
 | 
				
			||||||
 | 
					pub struct WithoutEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A ZIP entry reader which may implement decompression.
 | 
				
			||||||
 | 
					#[pin_project]
 | 
				
			||||||
 | 
					pub struct ZipEntryReader<'a, R, E> {
 | 
				
			||||||
 | 
					    #[pin]
 | 
				
			||||||
 | 
					    reader: HashedReader<CompressedReader<Take<OwnedReader<'a, R>>>>,
 | 
				
			||||||
 | 
					    entry: E,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R> ZipEntryReader<'a, R, WithoutEntry>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new entry reader from its required parameters (incl. an owned R).
 | 
				
			||||||
 | 
					    pub fn new_with_owned(reader: R, compression: Compression, size: u64) -> Self {
 | 
				
			||||||
 | 
					        let reader = HashedReader::new(CompressedReader::new(OwnedReader::Owned(reader).take(size), compression));
 | 
				
			||||||
 | 
					        Self { reader, entry: WithoutEntry }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Constructs a new entry reader from its required parameters (incl. a mutable borrow of an R).
 | 
				
			||||||
 | 
					    pub(crate) fn new_with_borrow(reader: &'a mut R, compression: Compression, size: u64) -> Self {
 | 
				
			||||||
 | 
					        let reader = HashedReader::new(CompressedReader::new(OwnedReader::Borrow(reader).take(size), compression));
 | 
				
			||||||
 | 
					        Self { reader, entry: WithoutEntry }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) fn into_with_entry(self, entry: &'a ZipEntry) -> ZipEntryReader<'a, R, WithEntry<'a>> {
 | 
				
			||||||
 | 
					        ZipEntryReader { reader: self.reader, entry: WithEntry(OwnedEntry::Borrow(entry)) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) fn into_with_entry_owned(self, entry: ZipEntry) -> ZipEntryReader<'a, R, WithEntry<'a>> {
 | 
				
			||||||
 | 
					        ZipEntryReader { reader: self.reader, entry: WithEntry(OwnedEntry::Owned(entry)) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R, E> AsyncRead for ZipEntryReader<'a, R, E>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    fn poll_read(self: Pin<&mut Self>, c: &mut Context<'_>, b: &mut [u8]) -> Poll<std::io::Result<usize>> {
 | 
				
			||||||
 | 
					        self.project().reader.poll_read(c, b)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R, E> ZipEntryReader<'a, R, E>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Computes and returns the CRC32 hash of bytes read by this reader so far.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This hash should only be computed once EOF has been reached.
 | 
				
			||||||
 | 
					    pub fn compute_hash(&mut self) -> u32 {
 | 
				
			||||||
 | 
					        self.reader.swap_and_compute_hash()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this reader and returns the inner value.
 | 
				
			||||||
 | 
					    pub(crate) fn into_inner(self) -> R {
 | 
				
			||||||
 | 
					        self.reader.into_inner().into_inner().into_inner().owned_into_inner()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<R> ZipEntryReader<'_, R, WithEntry<'_>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Returns an immutable reference to the associated entry data.
 | 
				
			||||||
 | 
					    pub fn entry(&self) -> &'_ ZipEntry {
 | 
				
			||||||
 | 
					        self.entry.0.entry()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Reads all bytes until EOF has been reached, appending them to buf, and verifies the CRC32 values.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This is a helper function synonymous to [`AsyncReadExt::read_to_end()`].
 | 
				
			||||||
 | 
					    pub async fn read_to_end_checked(&mut self, buf: &mut Vec<u8>) -> Result<usize> {
 | 
				
			||||||
 | 
					        let read = self.read_to_end(buf).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.compute_hash() == self.entry.0.entry().crc32() {
 | 
				
			||||||
 | 
					            Ok(read)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(ZipError::CRC32CheckError)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Reads all bytes until EOF has been reached, placing them into buf, and verifies the CRC32 values.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This is a helper function synonymous to [`AsyncReadExt::read_to_string()`].
 | 
				
			||||||
 | 
					    pub async fn read_to_string_checked(&mut self, buf: &mut String) -> Result<usize> {
 | 
				
			||||||
 | 
					        let read = self.read_to_string(buf).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.compute_hash() == self.entry.0.entry().crc32() {
 | 
				
			||||||
 | 
					            Ok(read)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(ZipError::CRC32CheckError)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum OwnedEntry<'a> {
 | 
				
			||||||
 | 
					    Owned(ZipEntry),
 | 
				
			||||||
 | 
					    Borrow(&'a ZipEntry),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> OwnedEntry<'a> {
 | 
				
			||||||
 | 
					    pub fn entry(&self) -> &'_ ZipEntry {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OwnedEntry::Owned(entry) => entry,
 | 
				
			||||||
 | 
					            OwnedEntry::Borrow(entry) => entry,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										56
									
								
								crates/async_zip/src/base/read/io/hashed.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								crates/async_zip/src/base/read/io/hashed.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::poll_result_ok;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{ready, Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crc32fast::Hasher;
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncRead;
 | 
				
			||||||
 | 
					use pin_project::pin_project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A wrapping reader which computes the CRC32 hash of data read via [`AsyncRead`].
 | 
				
			||||||
 | 
					#[pin_project]
 | 
				
			||||||
 | 
					pub(crate) struct HashedReader<R> {
 | 
				
			||||||
 | 
					    #[pin]
 | 
				
			||||||
 | 
					    pub(crate) reader: R,
 | 
				
			||||||
 | 
					    pub(crate) hasher: Hasher,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<R> HashedReader<R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new wrapping reader from a generic [`AsyncRead`] implementer.
 | 
				
			||||||
 | 
					    pub(crate) fn new(reader: R) -> Self {
 | 
				
			||||||
 | 
					        Self { reader, hasher: Hasher::default() }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Swaps the internal hasher and returns the computed CRC32 hash.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// The internal hasher is taken and replaced with a newly-constructed one. As a result, this method should only be
 | 
				
			||||||
 | 
					    /// called once EOF has been reached and it's known that no more data will be read, else the computed hash(s) won't
 | 
				
			||||||
 | 
					    /// accurately represent the data read in.
 | 
				
			||||||
 | 
					    pub(crate) fn swap_and_compute_hash(&mut self) -> u32 {
 | 
				
			||||||
 | 
					        std::mem::take(&mut self.hasher).finalize()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this reader and returns the inner value.
 | 
				
			||||||
 | 
					    pub(crate) fn into_inner(self) -> R {
 | 
				
			||||||
 | 
					        self.reader
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<R> AsyncRead for HashedReader<R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    fn poll_read(self: Pin<&mut Self>, c: &mut Context<'_>, b: &mut [u8]) -> Poll<std::io::Result<usize>> {
 | 
				
			||||||
 | 
					        let project = self.project();
 | 
				
			||||||
 | 
					        let written = poll_result_ok!(ready!(project.reader.poll_read(c, b)));
 | 
				
			||||||
 | 
					        project.hasher.update(&b[..written]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Poll::Ready(Ok(written))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								crates/async_zip/src/base/read/io/locator.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								crates/async_zip/src/base/read/io/locator.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,96 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! <https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4316>
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! As with other ZIP libraries, we face the predicament that the end of central directory record may contain a
 | 
				
			||||||
 | 
					//! variable-length file comment. As a result, we cannot just make the assumption that the start of this record is
 | 
				
			||||||
 | 
					//! 18 bytes (the length of the EOCDR) offset from the end of the data - we must locate it ourselves.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! The `zip-rs` crate handles this by reading in reverse from the end of the data. This involves seeking backwards
 | 
				
			||||||
 | 
					//! by a single byte each iteration and reading 4 bytes into a u32. Whether this is performant/acceptable within a
 | 
				
			||||||
 | 
					//! a non-async context, I'm unsure, but it isn't desirable within an async context. Especially since we cannot just
 | 
				
			||||||
 | 
					//! place a [`BufReader`] infront of the upstream reader (as its internal buffer is invalidated on each seek).
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Reading in reverse is still desirable as the use of file comments is limited and they're unlikely to be large.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! The below method is one that compromises on these two contention points. Please submit an issue or PR if you know
 | 
				
			||||||
 | 
					//! of a better algorithm for this (and have tested/verified its performance).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use futures_lite::io::BufReader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::error::{Result as ZipResult, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::consts::{EOCDR_LENGTH, EOCDR_SIGNATURE, SIGNATURE_LENGTH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, SeekFrom};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The buffer size used when locating the EOCDR, equal to 2KiB.
 | 
				
			||||||
 | 
					const BUFFER_SIZE: usize = 2048;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The upper bound of where the EOCDR signature cannot be located.
 | 
				
			||||||
 | 
					const EOCDR_UPPER_BOUND: u64 = EOCDR_LENGTH as u64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The lower bound of where the EOCDR signature cannot be located.
 | 
				
			||||||
 | 
					const EOCDR_LOWER_BOUND: u64 = EOCDR_UPPER_BOUND + SIGNATURE_LENGTH as u64 + u16::MAX as u64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Locate the `end of central directory record` offset, if one exists.
 | 
				
			||||||
 | 
					/// The returned offset excludes the signature (4 bytes)
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This method involves buffered reading in reverse and reverse linear searching along those buffers for the EOCDR
 | 
				
			||||||
 | 
					/// signature. As a result of this buffered approach, we reduce seeks when compared to `zip-rs`'s method by a factor
 | 
				
			||||||
 | 
					/// of the buffer size. We also then don't have to do individual u32 reads against the upstream reader.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Whilst I haven't done any in-depth benchmarks, when reading a ZIP file with the maximum length comment, this method
 | 
				
			||||||
 | 
					/// saw a reduction in location time by a factor of 500 when compared with the `zip-rs` method.
 | 
				
			||||||
 | 
					pub async fn eocdr<R>(mut reader: R) -> ZipResult<u64>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + AsyncSeek + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    let length = reader.seek(SeekFrom::End(0)).await?;
 | 
				
			||||||
 | 
					    let signature = &EOCDR_SIGNATURE.to_le_bytes();
 | 
				
			||||||
 | 
					    let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut position = length.saturating_sub((EOCDR_LENGTH + BUFFER_SIZE) as u64);
 | 
				
			||||||
 | 
					    reader.seek(SeekFrom::Start(position)).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					        let read = reader.read(&mut buffer).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(match_index) = reverse_search_buffer(&buffer[..read], signature) {
 | 
				
			||||||
 | 
					            return Ok(position + (match_index + 1) as u64);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // If we hit the start of the data or the lower bound, we're unable to locate the EOCDR.
 | 
				
			||||||
 | 
					        if position == 0 || position <= length.saturating_sub(EOCDR_LOWER_BOUND) {
 | 
				
			||||||
 | 
					            return Err(ZipError::UnableToLocateEOCDR);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // To handle the case where the EOCDR signature crosses buffer boundaries, we simply overlap reads by the
 | 
				
			||||||
 | 
					        // signature length. This significantly reduces the complexity of handling partial matches with very little
 | 
				
			||||||
 | 
					        // overhead.
 | 
				
			||||||
 | 
					        position = position.saturating_sub((BUFFER_SIZE - SIGNATURE_LENGTH) as u64);
 | 
				
			||||||
 | 
					        reader.seek(SeekFrom::Start(position)).await?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A naive reverse linear search along the buffer for the specified signature bytes.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This is already surprisingly performant. For instance, using memchr::memchr() to match for the first byte of the
 | 
				
			||||||
 | 
					/// signature, and then manual byte comparisons for the remaining signature bytes was actually slower by a factor of
 | 
				
			||||||
 | 
					/// 2.25. This method was explored as tokio's `read_until()` implementation uses memchr::memchr().
 | 
				
			||||||
 | 
					pub(crate) fn reverse_search_buffer(buffer: &[u8], signature: &[u8]) -> Option<usize> {
 | 
				
			||||||
 | 
					    'outer: for index in (0..buffer.len()).rev() {
 | 
				
			||||||
 | 
					        for (signature_index, signature_byte) in signature.iter().rev().enumerate() {
 | 
				
			||||||
 | 
					            if let Some(next_index) = index.checked_sub(signature_index) {
 | 
				
			||||||
 | 
					                if buffer[next_index] != *signature_byte {
 | 
				
			||||||
 | 
					                    continue 'outer;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                break 'outer;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Some(index);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    None
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								crates/async_zip/src/base/read/io/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								crates/async_zip/src/base/read/io/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,88 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod combined_record;
 | 
				
			||||||
 | 
					pub(crate) mod compressed;
 | 
				
			||||||
 | 
					pub(crate) mod entry;
 | 
				
			||||||
 | 
					pub(crate) mod hashed;
 | 
				
			||||||
 | 
					pub(crate) mod locator;
 | 
				
			||||||
 | 
					pub(crate) mod owned;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    future::Future,
 | 
				
			||||||
 | 
					    io::ErrorKind,
 | 
				
			||||||
 | 
					    pin::Pin,
 | 
				
			||||||
 | 
					    task::{ready, Context, Poll},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use combined_record::CombinedCentralDirectoryRecord;
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncBufRead;
 | 
				
			||||||
 | 
					use pin_project::pin_project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    spec::consts::{DATA_DESCRIPTOR_LENGTH, DATA_DESCRIPTOR_SIGNATURE, SIGNATURE_LENGTH},
 | 
				
			||||||
 | 
					    string::{StringEncoding, ZipString},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncRead, AsyncReadExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Read and return a dynamic length string from a reader which impls AsyncRead.
 | 
				
			||||||
 | 
					pub(crate) async fn read_string<R>(reader: R, length: usize, encoding: StringEncoding) -> std::io::Result<ZipString>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    Ok(ZipString::new(read_bytes(reader, length).await?, encoding))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Read and return a dynamic length vector of bytes from a reader which impls AsyncRead.
 | 
				
			||||||
 | 
					pub(crate) async fn read_bytes<R>(reader: R, length: usize) -> std::io::Result<Vec<u8>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    let mut buffer = Vec::with_capacity(length);
 | 
				
			||||||
 | 
					    reader.take(length as u64).read_to_end(&mut buffer).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(buffer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[pin_project]
 | 
				
			||||||
 | 
					pub(crate) struct ConsumeDataDescriptor<'a, R>(#[pin] pub(crate) &'a mut R);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<R> Future for ConsumeDataDescriptor<'_, R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    type Output = std::io::Result<()>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
 | 
				
			||||||
 | 
					        let mut project = self.project();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let data = poll_result_ok!(ready!(project.0.as_mut().poll_fill_buf(cx)));
 | 
				
			||||||
 | 
					        let signature = data.get(0..4).ok_or(ErrorKind::UnexpectedEof)?;
 | 
				
			||||||
 | 
					        let mut consumed = DATA_DESCRIPTOR_LENGTH;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if signature == DATA_DESCRIPTOR_SIGNATURE.to_le_bytes() {
 | 
				
			||||||
 | 
					            consumed += SIGNATURE_LENGTH;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if consumed > data.len() {
 | 
				
			||||||
 | 
					            return Poll::Ready(Err(ErrorKind::UnexpectedEof.into()));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        project.0.as_mut().consume(consumed);
 | 
				
			||||||
 | 
					        Poll::Ready(Ok(()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A macro that returns the inner value of an Ok or early-returns in the case of an Err.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This is almost identical to the ? operator but handles the situation when a Result is used in combination with
 | 
				
			||||||
 | 
					/// Poll (eg. tokio's IO traits such as AsyncRead).
 | 
				
			||||||
 | 
					macro_rules! poll_result_ok {
 | 
				
			||||||
 | 
					    ($poll:expr) => {
 | 
				
			||||||
 | 
					        match $poll {
 | 
				
			||||||
 | 
					            Ok(inner) => inner,
 | 
				
			||||||
 | 
					            Err(err) => return Poll::Ready(Err(err)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use poll_result_ok;
 | 
				
			||||||
							
								
								
									
										62
									
								
								crates/async_zip/src/base/read/io/owned.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								crates/async_zip/src/base/read/io/owned.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,62 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncBufRead, AsyncRead};
 | 
				
			||||||
 | 
					use pin_project::pin_project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A wrapping reader which holds an owned R or a mutable borrow to R.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This is used to represent whether the supplied reader can be acted on concurrently or not (with an owned value
 | 
				
			||||||
 | 
					/// suggesting that R implements some method of synchronisation & cloning).
 | 
				
			||||||
 | 
					#[pin_project(project = OwnedReaderProj)]
 | 
				
			||||||
 | 
					pub(crate) enum OwnedReader<'a, R> {
 | 
				
			||||||
 | 
					    Owned(#[pin] R),
 | 
				
			||||||
 | 
					    Borrow(#[pin] &'a mut R),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R> OwnedReader<'a, R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Consumes an owned reader and returns the inner value.
 | 
				
			||||||
 | 
					    pub(crate) fn owned_into_inner(self) -> R {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            OwnedReader::Owned(inner) => inner,
 | 
				
			||||||
 | 
					            OwnedReader::Borrow(_) => panic!("not OwnedReader::Owned value"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R> AsyncBufRead for OwnedReader<'a, R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<&[u8]>> {
 | 
				
			||||||
 | 
					        match self.project() {
 | 
				
			||||||
 | 
					            OwnedReaderProj::Owned(inner) => inner.poll_fill_buf(cx),
 | 
				
			||||||
 | 
					            OwnedReaderProj::Borrow(inner) => inner.poll_fill_buf(cx),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn consume(self: Pin<&mut Self>, amt: usize) {
 | 
				
			||||||
 | 
					        match self.project() {
 | 
				
			||||||
 | 
					            OwnedReaderProj::Owned(inner) => inner.consume(amt),
 | 
				
			||||||
 | 
					            OwnedReaderProj::Borrow(inner) => inner.consume(amt),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R> AsyncRead for OwnedReader<'a, R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    fn poll_read(self: Pin<&mut Self>, c: &mut Context<'_>, b: &mut [u8]) -> Poll<std::io::Result<usize>> {
 | 
				
			||||||
 | 
					        match self.project() {
 | 
				
			||||||
 | 
					            OwnedReaderProj::Owned(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					            OwnedReaderProj::Borrow(inner) => inner.poll_read(c, b),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										147
									
								
								crates/async_zip/src/base/read/mem.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								crates/async_zip/src/base/read/mem.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,147 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A concurrent ZIP reader which acts over an owned vector of bytes.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Concurrency is achieved as a result of:
 | 
				
			||||||
 | 
					//! - Wrapping the provided vector of bytes within an [`Arc`] to allow shared ownership.
 | 
				
			||||||
 | 
					//! - Wrapping this [`Arc`] around a [`Cursor`] when reading (as the [`Arc`] can deref and coerce into a `&[u8]`).
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Usage
 | 
				
			||||||
 | 
					//! Unlike the [`seek`] module, we no longer hold a mutable reference to any inner reader which in turn, allows the
 | 
				
			||||||
 | 
					//! construction of concurrent [`ZipEntryReader`]s. Though, note that each individual [`ZipEntryReader`] cannot be sent
 | 
				
			||||||
 | 
					//! between thread boundaries due to the masked lifetime requirement. Therefore, the overarching [`ZipFileReader`]
 | 
				
			||||||
 | 
					//! should be cloned and moved into those contexts when needed.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Concurrent Example
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # use async_zip::base::read::mem::ZipFileReader;
 | 
				
			||||||
 | 
					//! # use async_zip::error::Result;
 | 
				
			||||||
 | 
					//! # use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! async fn run() -> Result<()> {
 | 
				
			||||||
 | 
					//!     let reader = ZipFileReader::new(Vec::new()).await?;
 | 
				
			||||||
 | 
					//!     let result = tokio::join!(read(&reader, 0), read(&reader, 1));
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     let data_0 = result.0?;
 | 
				
			||||||
 | 
					//!     let data_1 = result.1?;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     // Use data within current scope.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     Ok(())
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! async fn read(reader: &ZipFileReader, index: usize) -> Result<Vec<u8>> {
 | 
				
			||||||
 | 
					//!     let mut entry = reader.reader_without_entry(index).await?;
 | 
				
			||||||
 | 
					//!     let mut data = Vec::new();
 | 
				
			||||||
 | 
					//!     entry.read_to_end(&mut data).await?;
 | 
				
			||||||
 | 
					//!     Ok(data)
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Parallel Example
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # use async_zip::base::read::mem::ZipFileReader;
 | 
				
			||||||
 | 
					//! # use async_zip::error::Result;
 | 
				
			||||||
 | 
					//! # use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! async fn run() -> Result<()> {
 | 
				
			||||||
 | 
					//!     let reader = ZipFileReader::new(Vec::new()).await?;
 | 
				
			||||||
 | 
					//!     
 | 
				
			||||||
 | 
					//!     let handle_0 = tokio::spawn(read(reader.clone(), 0));
 | 
				
			||||||
 | 
					//!     let handle_1 = tokio::spawn(read(reader.clone(), 1));
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     let data_0 = handle_0.await.expect("thread panicked")?;
 | 
				
			||||||
 | 
					//!     let data_1 = handle_1.await.expect("thread panicked")?;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     // Use data within current scope.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     Ok(())
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! async fn read(reader: ZipFileReader, index: usize) -> Result<Vec<u8>> {
 | 
				
			||||||
 | 
					//!     let mut entry = reader.reader_without_entry(index).await?;
 | 
				
			||||||
 | 
					//!     let mut data = Vec::new();
 | 
				
			||||||
 | 
					//!     entry.read_to_end(&mut data).await?;
 | 
				
			||||||
 | 
					//!     Ok(data)
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use crate::base::read::seek;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::entry::ZipEntryReader;
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use crate::file::ZipFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::io::entry::{WithEntry, WithoutEntry};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Inner {
 | 
				
			||||||
 | 
					    data: Vec<u8>,
 | 
				
			||||||
 | 
					    file: ZipFile,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A concurrent ZIP reader which acts over an owned vector of bytes.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct ZipFileReader {
 | 
				
			||||||
 | 
					    inner: Arc<Inner>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipFileReader {
 | 
				
			||||||
 | 
					    /// Constructs a new ZIP reader from an owned vector of bytes.
 | 
				
			||||||
 | 
					    pub async fn new(data: Vec<u8>) -> Result<ZipFileReader> {
 | 
				
			||||||
 | 
					        let file = crate::base::read::file(Cursor::new(&data)).await?;
 | 
				
			||||||
 | 
					        Ok(ZipFileReader::from_raw_parts(data, file))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Constructs a ZIP reader from an owned vector of bytes and ZIP file information derived from those bytes.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Providing a [`ZipFile`] that wasn't derived from those bytes may lead to inaccurate parsing.
 | 
				
			||||||
 | 
					    pub fn from_raw_parts(data: Vec<u8>, file: ZipFile) -> ZipFileReader {
 | 
				
			||||||
 | 
					        ZipFileReader { inner: Arc::new(Inner { data, file }) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns this ZIP file's information.
 | 
				
			||||||
 | 
					    pub fn file(&self) -> &ZipFile {
 | 
				
			||||||
 | 
					        &self.inner.file
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the raw bytes provided to the reader during construction.
 | 
				
			||||||
 | 
					    pub fn data(&self) -> &[u8] {
 | 
				
			||||||
 | 
					        &self.inner.data
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a new entry reader if the provided index is valid.
 | 
				
			||||||
 | 
					    pub async fn reader_without_entry(&self, index: usize) -> Result<ZipEntryReader<Cursor<&[u8]>, WithoutEntry>> {
 | 
				
			||||||
 | 
					        let stored_entry = self.inner.file.entries.get(index).ok_or(ZipError::EntryIndexOutOfBounds)?;
 | 
				
			||||||
 | 
					        let mut cursor = Cursor::new(&self.inner.data[..]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stored_entry.seek_to_data_offset(&mut cursor).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(ZipEntryReader::new_with_owned(
 | 
				
			||||||
 | 
					            cursor,
 | 
				
			||||||
 | 
					            stored_entry.entry.compression(),
 | 
				
			||||||
 | 
					            stored_entry.entry.compressed_size(),
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a new entry reader if the provided index is valid.
 | 
				
			||||||
 | 
					    pub async fn reader_with_entry(&self, index: usize) -> Result<ZipEntryReader<Cursor<&[u8]>, WithEntry<'_>>> {
 | 
				
			||||||
 | 
					        let stored_entry = self.inner.file.entries.get(index).ok_or(ZipError::EntryIndexOutOfBounds)?;
 | 
				
			||||||
 | 
					        let mut cursor = Cursor::new(&self.inner.data[..]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stored_entry.seek_to_data_offset(&mut cursor).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reader = ZipEntryReader::new_with_owned(
 | 
				
			||||||
 | 
					            cursor,
 | 
				
			||||||
 | 
					            stored_entry.entry.compression(),
 | 
				
			||||||
 | 
					            stored_entry.entry.compressed_size(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(reader.into_with_entry(stored_entry))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										320
									
								
								crates/async_zip/src/base/read/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								crates/async_zip/src/base/read/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,320 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022-2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A module which supports reading ZIP files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod mem;
 | 
				
			||||||
 | 
					pub mod seek;
 | 
				
			||||||
 | 
					pub mod stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod io;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::ZipString;
 | 
				
			||||||
 | 
					// Re-exported as part of the public API.
 | 
				
			||||||
 | 
					pub use crate::base::read::io::entry::WithEntry;
 | 
				
			||||||
 | 
					pub use crate::base::read::io::entry::WithoutEntry;
 | 
				
			||||||
 | 
					pub use crate::base::read::io::entry::ZipEntryReader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::date::ZipDateTime;
 | 
				
			||||||
 | 
					use crate::entry::{StoredZipEntry, ZipEntry};
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use crate::file::ZipFile;
 | 
				
			||||||
 | 
					use crate::spec::attribute::AttributeCompatibility;
 | 
				
			||||||
 | 
					use crate::spec::consts::LFH_LENGTH;
 | 
				
			||||||
 | 
					use crate::spec::consts::{CDH_SIGNATURE, LFH_SIGNATURE, NON_ZIP64_MAX_SIZE, SIGNATURE_LENGTH, ZIP64_EOCDL_LENGTH};
 | 
				
			||||||
 | 
					use crate::spec::header::InfoZipUnicodeCommentExtraField;
 | 
				
			||||||
 | 
					use crate::spec::header::InfoZipUnicodePathExtraField;
 | 
				
			||||||
 | 
					use crate::spec::header::{
 | 
				
			||||||
 | 
					    CentralDirectoryRecord, EndOfCentralDirectoryHeader, ExtraField, LocalFileHeader,
 | 
				
			||||||
 | 
					    Zip64EndOfCentralDirectoryLocator, Zip64EndOfCentralDirectoryRecord, Zip64ExtendedInformationExtraField,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::spec::Compression;
 | 
				
			||||||
 | 
					use crate::string::StringEncoding;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::CombinedCentralDirectoryRecord;
 | 
				
			||||||
 | 
					use crate::spec::parse::parse_extra_fields;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, SeekFrom};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) async fn file<R>(mut reader: R) -> Result<ZipFile>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + AsyncSeek + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    // First find and parse the EOCDR.
 | 
				
			||||||
 | 
					    let eocdr_offset = crate::base::read::io::locator::eocdr(&mut reader).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reader.seek(SeekFrom::Start(eocdr_offset)).await?;
 | 
				
			||||||
 | 
					    let eocdr = EndOfCentralDirectoryHeader::from_reader(&mut reader).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let comment = io::read_string(&mut reader, eocdr.file_comm_length.into(), crate::StringEncoding::Utf8).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check the 20 bytes before the EOCDR for the Zip64 EOCDL, plus an extra 4 bytes because the offset
 | 
				
			||||||
 | 
					    // does not include the signature. If the ECODL exists we are dealing with a Zip64 file.
 | 
				
			||||||
 | 
					    let (eocdr, zip64) = match eocdr_offset.checked_sub(ZIP64_EOCDL_LENGTH + SIGNATURE_LENGTH as u64) {
 | 
				
			||||||
 | 
					        None => (CombinedCentralDirectoryRecord::from(&eocdr), false),
 | 
				
			||||||
 | 
					        Some(offset) => {
 | 
				
			||||||
 | 
					            reader.seek(SeekFrom::Start(offset)).await?;
 | 
				
			||||||
 | 
					            let zip64_locator = Zip64EndOfCentralDirectoryLocator::try_from_reader(&mut reader).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match zip64_locator {
 | 
				
			||||||
 | 
					                Some(locator) => {
 | 
				
			||||||
 | 
					                    reader.seek(SeekFrom::Start(locator.relative_offset + SIGNATURE_LENGTH as u64)).await?;
 | 
				
			||||||
 | 
					                    let zip64_eocdr = Zip64EndOfCentralDirectoryRecord::from_reader(&mut reader).await?;
 | 
				
			||||||
 | 
					                    (CombinedCentralDirectoryRecord::combine(eocdr, zip64_eocdr), true)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                None => (CombinedCentralDirectoryRecord::from(&eocdr), false),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Outdated feature so unlikely to ever make it into this crate.
 | 
				
			||||||
 | 
					    if eocdr.disk_number != eocdr.disk_number_start_of_cd
 | 
				
			||||||
 | 
					        || eocdr.num_entries_in_directory != eocdr.num_entries_in_directory_on_disk
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Err(ZipError::FeatureNotSupported("Spanned/split files"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Find and parse the central directory.
 | 
				
			||||||
 | 
					    reader.seek(SeekFrom::Start(eocdr.offset_of_start_of_directory)).await?;
 | 
				
			||||||
 | 
					    let entries = crate::base::read::cd(reader, eocdr.num_entries_in_directory, zip64).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(ZipFile { entries, comment, zip64 })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) async fn cd<R>(mut reader: R, num_of_entries: u64, zip64: bool) -> Result<Vec<StoredZipEntry>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    let num_of_entries = num_of_entries.try_into().map_err(|_| ZipError::TargetZip64NotSupported)?;
 | 
				
			||||||
 | 
					    let mut entries = Vec::with_capacity(num_of_entries);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for _ in 0..num_of_entries {
 | 
				
			||||||
 | 
					        let entry = cd_record(&mut reader, zip64).await?;
 | 
				
			||||||
 | 
					        entries.push(entry);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(entries)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) fn get_zip64_extra_field(extra_fields: &[ExtraField]) -> Option<&Zip64ExtendedInformationExtraField> {
 | 
				
			||||||
 | 
					    for field in extra_fields {
 | 
				
			||||||
 | 
					        if let ExtraField::Zip64ExtendedInformation(zip64field) = field {
 | 
				
			||||||
 | 
					            return Some(zip64field);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    None
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) fn get_zip64_extra_field_mut(
 | 
				
			||||||
 | 
					    extra_fields: &mut [ExtraField],
 | 
				
			||||||
 | 
					) -> Option<&mut Zip64ExtendedInformationExtraField> {
 | 
				
			||||||
 | 
					    for field in extra_fields {
 | 
				
			||||||
 | 
					        if let ExtraField::Zip64ExtendedInformation(zip64field) = field {
 | 
				
			||||||
 | 
					            return Some(zip64field);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    None
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_combined_sizes(
 | 
				
			||||||
 | 
					    uncompressed_size: u32,
 | 
				
			||||||
 | 
					    compressed_size: u32,
 | 
				
			||||||
 | 
					    extra_field: &Option<&Zip64ExtendedInformationExtraField>,
 | 
				
			||||||
 | 
					) -> Result<(u64, u64)> {
 | 
				
			||||||
 | 
					    let mut uncompressed_size = uncompressed_size as u64;
 | 
				
			||||||
 | 
					    let mut compressed_size = compressed_size as u64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Some(extra_field) = extra_field {
 | 
				
			||||||
 | 
					        if let Some(s) = extra_field.uncompressed_size {
 | 
				
			||||||
 | 
					            uncompressed_size = s;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(s) = extra_field.compressed_size {
 | 
				
			||||||
 | 
					            compressed_size = s;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok((uncompressed_size, compressed_size))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) async fn cd_record<R>(mut reader: R, _zip64: bool) -> Result<StoredZipEntry>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    crate::utils::assert_signature(&mut reader, CDH_SIGNATURE).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let header = CentralDirectoryRecord::from_reader(&mut reader).await?;
 | 
				
			||||||
 | 
					    let header_size = (SIGNATURE_LENGTH + LFH_LENGTH) as u64;
 | 
				
			||||||
 | 
					    let trailing_size = header.file_name_length as u64 + header.extra_field_length as u64;
 | 
				
			||||||
 | 
					    let filename_basic = io::read_bytes(&mut reader, header.file_name_length.into()).await?;
 | 
				
			||||||
 | 
					    let compression = Compression::try_from(header.compression)?;
 | 
				
			||||||
 | 
					    let extra_field = io::read_bytes(&mut reader, header.extra_field_length.into()).await?;
 | 
				
			||||||
 | 
					    let extra_fields = parse_extra_fields(extra_field, header.uncompressed_size, header.compressed_size)?;
 | 
				
			||||||
 | 
					    let comment_basic = io::read_bytes(reader, header.file_comment_length.into()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let zip64_extra_field = get_zip64_extra_field(&extra_fields);
 | 
				
			||||||
 | 
					    let (uncompressed_size, compressed_size) =
 | 
				
			||||||
 | 
					        get_combined_sizes(header.uncompressed_size, header.compressed_size, &zip64_extra_field)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut file_offset = header.lh_offset as u64;
 | 
				
			||||||
 | 
					    if let Some(zip64_extra_field) = zip64_extra_field {
 | 
				
			||||||
 | 
					        if file_offset == NON_ZIP64_MAX_SIZE as u64 {
 | 
				
			||||||
 | 
					            if let Some(offset) = zip64_extra_field.relative_header_offset {
 | 
				
			||||||
 | 
					                file_offset = offset;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let filename = detect_filename(filename_basic, header.flags.filename_unicode, extra_fields.as_ref());
 | 
				
			||||||
 | 
					    let comment = detect_comment(comment_basic, header.flags.filename_unicode, extra_fields.as_ref());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let entry = ZipEntry {
 | 
				
			||||||
 | 
					        filename,
 | 
				
			||||||
 | 
					        compression,
 | 
				
			||||||
 | 
					        #[cfg(any(
 | 
				
			||||||
 | 
					            feature = "deflate",
 | 
				
			||||||
 | 
					            feature = "bzip2",
 | 
				
			||||||
 | 
					            feature = "zstd",
 | 
				
			||||||
 | 
					            feature = "lzma",
 | 
				
			||||||
 | 
					            feature = "xz",
 | 
				
			||||||
 | 
					            feature = "deflate64"
 | 
				
			||||||
 | 
					        ))]
 | 
				
			||||||
 | 
					        compression_level: async_compression::Level::Default,
 | 
				
			||||||
 | 
					        attribute_compatibility: AttributeCompatibility::Unix,
 | 
				
			||||||
 | 
					        // FIXME: Default to Unix for the moment
 | 
				
			||||||
 | 
					        crc32: header.crc,
 | 
				
			||||||
 | 
					        uncompressed_size,
 | 
				
			||||||
 | 
					        compressed_size,
 | 
				
			||||||
 | 
					        last_modification_date: ZipDateTime { date: header.mod_date, time: header.mod_time },
 | 
				
			||||||
 | 
					        internal_file_attribute: header.inter_attr,
 | 
				
			||||||
 | 
					        external_file_attribute: header.exter_attr,
 | 
				
			||||||
 | 
					        extra_fields,
 | 
				
			||||||
 | 
					        comment,
 | 
				
			||||||
 | 
					        data_descriptor: header.flags.data_descriptor,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(StoredZipEntry { entry, file_offset, header_size: header_size + trailing_size })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) async fn lfh<R>(mut reader: R) -> Result<Option<ZipEntry>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    let signature = {
 | 
				
			||||||
 | 
					        let mut buffer = [0; 4];
 | 
				
			||||||
 | 
					        reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					        u32::from_le_bytes(buffer)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    match signature {
 | 
				
			||||||
 | 
					        actual if actual == LFH_SIGNATURE => (),
 | 
				
			||||||
 | 
					        actual if actual == CDH_SIGNATURE => return Ok(None),
 | 
				
			||||||
 | 
					        actual => return Err(ZipError::UnexpectedHeaderError(actual, LFH_SIGNATURE)),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let header = LocalFileHeader::from_reader(&mut reader).await?;
 | 
				
			||||||
 | 
					    let filename_basic = io::read_bytes(&mut reader, header.file_name_length.into()).await?;
 | 
				
			||||||
 | 
					    let compression = Compression::try_from(header.compression)?;
 | 
				
			||||||
 | 
					    let extra_field = io::read_bytes(&mut reader, header.extra_field_length.into()).await?;
 | 
				
			||||||
 | 
					    let extra_fields = parse_extra_fields(extra_field, header.uncompressed_size, header.compressed_size)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let zip64_extra_field = get_zip64_extra_field(&extra_fields);
 | 
				
			||||||
 | 
					    let (uncompressed_size, compressed_size) =
 | 
				
			||||||
 | 
					        get_combined_sizes(header.uncompressed_size, header.compressed_size, &zip64_extra_field)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if header.flags.data_descriptor && compression == Compression::Stored {
 | 
				
			||||||
 | 
					        return Err(ZipError::FeatureNotSupported(
 | 
				
			||||||
 | 
					            "stream reading entries with data descriptors & Stored compression mode",
 | 
				
			||||||
 | 
					        ));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if header.flags.encrypted {
 | 
				
			||||||
 | 
					        return Err(ZipError::FeatureNotSupported("encryption"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let filename = detect_filename(filename_basic, header.flags.filename_unicode, extra_fields.as_ref());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let entry = ZipEntry {
 | 
				
			||||||
 | 
					        filename,
 | 
				
			||||||
 | 
					        compression,
 | 
				
			||||||
 | 
					        #[cfg(any(
 | 
				
			||||||
 | 
					            feature = "deflate",
 | 
				
			||||||
 | 
					            feature = "bzip2",
 | 
				
			||||||
 | 
					            feature = "zstd",
 | 
				
			||||||
 | 
					            feature = "lzma",
 | 
				
			||||||
 | 
					            feature = "xz",
 | 
				
			||||||
 | 
					            feature = "deflate64"
 | 
				
			||||||
 | 
					        ))]
 | 
				
			||||||
 | 
					        compression_level: async_compression::Level::Default,
 | 
				
			||||||
 | 
					        attribute_compatibility: AttributeCompatibility::Unix,
 | 
				
			||||||
 | 
					        // FIXME: Default to Unix for the moment
 | 
				
			||||||
 | 
					        crc32: header.crc,
 | 
				
			||||||
 | 
					        uncompressed_size,
 | 
				
			||||||
 | 
					        compressed_size,
 | 
				
			||||||
 | 
					        last_modification_date: ZipDateTime { date: header.mod_date, time: header.mod_time },
 | 
				
			||||||
 | 
					        internal_file_attribute: 0,
 | 
				
			||||||
 | 
					        external_file_attribute: 0,
 | 
				
			||||||
 | 
					        extra_fields,
 | 
				
			||||||
 | 
					        comment: String::new().into(),
 | 
				
			||||||
 | 
					        data_descriptor: header.flags.data_descriptor,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(Some(entry))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn detect_comment(basic: Vec<u8>, basic_is_utf8: bool, extra_fields: &[ExtraField]) -> ZipString {
 | 
				
			||||||
 | 
					    if basic_is_utf8 {
 | 
				
			||||||
 | 
					        ZipString::new(basic, StringEncoding::Utf8)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let unicode_extra = extra_fields.iter().find_map(|field| match field {
 | 
				
			||||||
 | 
					            ExtraField::InfoZipUnicodeComment(InfoZipUnicodeCommentExtraField::V1 { crc32, unicode }) => {
 | 
				
			||||||
 | 
					                if *crc32 == crc32fast::hash(&basic) {
 | 
				
			||||||
 | 
					                    Some(std::string::String::from_utf8(unicode.clone()))
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    None
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => None,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if let Some(Ok(s)) = unicode_extra {
 | 
				
			||||||
 | 
					            ZipString::new_with_alternative(s, basic)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Do not treat as UTF-8 if UTF-8 flags are not set,
 | 
				
			||||||
 | 
					            // some string in MBCS may be valid UTF-8 in form, but they are not in truth.
 | 
				
			||||||
 | 
					            if basic.is_ascii() {
 | 
				
			||||||
 | 
					                // SAFETY:
 | 
				
			||||||
 | 
					                // a valid ASCII string is always a valid UTF-8 string
 | 
				
			||||||
 | 
					                unsafe { std::string::String::from_utf8_unchecked(basic).into() }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                ZipString::new(basic, StringEncoding::Raw)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn detect_filename(basic: Vec<u8>, basic_is_utf8: bool, extra_fields: &[ExtraField]) -> ZipString {
 | 
				
			||||||
 | 
					    if basic_is_utf8 {
 | 
				
			||||||
 | 
					        ZipString::new(basic, StringEncoding::Utf8)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        let unicode_extra = extra_fields.iter().find_map(|field| match field {
 | 
				
			||||||
 | 
					            ExtraField::InfoZipUnicodePath(InfoZipUnicodePathExtraField::V1 { crc32, unicode }) => {
 | 
				
			||||||
 | 
					                if *crc32 == crc32fast::hash(&basic) {
 | 
				
			||||||
 | 
					                    Some(std::string::String::from_utf8(unicode.clone()))
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    None
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => None,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if let Some(Ok(s)) = unicode_extra {
 | 
				
			||||||
 | 
					            ZipString::new_with_alternative(s, basic)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Do not treat as UTF-8 if UTF-8 flags are not set,
 | 
				
			||||||
 | 
					            // some string in MBCS may be valid UTF-8 in form, but they are not in truth.
 | 
				
			||||||
 | 
					            if basic.is_ascii() {
 | 
				
			||||||
 | 
					                // SAFETY:
 | 
				
			||||||
 | 
					                // a valid ASCII string is always a valid UTF-8 string
 | 
				
			||||||
 | 
					                unsafe { std::string::String::from_utf8_unchecked(basic).into() }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                ZipString::new(basic, StringEncoding::Raw)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										140
									
								
								crates/async_zip/src/base/read/seek.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								crates/async_zip/src/base/read/seek.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,140 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A ZIP reader which acts over a seekable source.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Example
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # use async_zip::base::read::seek::ZipFileReader;
 | 
				
			||||||
 | 
					//! # use async_zip::error::Result;
 | 
				
			||||||
 | 
					//! # use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					//! # use tokio::fs::File;
 | 
				
			||||||
 | 
					//! # use tokio_util::compat::TokioAsyncReadCompatExt;
 | 
				
			||||||
 | 
					//! # use tokio::io::BufReader;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! async fn run() -> Result<()> {
 | 
				
			||||||
 | 
					//!     let mut data = BufReader::new(File::open("./foo.zip").await?);
 | 
				
			||||||
 | 
					//!     let mut reader = ZipFileReader::new(data.compat()).await?;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     let mut data = Vec::new();
 | 
				
			||||||
 | 
					//!     let mut entry = reader.reader_without_entry(0).await?;
 | 
				
			||||||
 | 
					//!     entry.read_to_end(&mut data).await?;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     // Use data within current scope.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     Ok(())
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::entry::ZipEntryReader;
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use crate::file::ZipFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					use crate::tokio::read::seek::ZipFileReader as TokioZipFileReader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncBufRead, AsyncSeek};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					use tokio_util::compat::{Compat, TokioAsyncReadCompatExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::io::entry::{WithEntry, WithoutEntry};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A ZIP reader which acts over a seekable source.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct ZipFileReader<R> {
 | 
				
			||||||
 | 
					    reader: R,
 | 
				
			||||||
 | 
					    file: ZipFile,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<R> ZipFileReader<R>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + AsyncSeek + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new ZIP reader from a seekable source.
 | 
				
			||||||
 | 
					    pub async fn new(mut reader: R) -> Result<ZipFileReader<R>> {
 | 
				
			||||||
 | 
					        let file = crate::base::read::file(&mut reader).await?;
 | 
				
			||||||
 | 
					        Ok(ZipFileReader::from_raw_parts(reader, file))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Constructs a ZIP reader from a seekable source and ZIP file information derived from that source.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Providing a [`ZipFile`] that wasn't derived from that source may lead to inaccurate parsing.
 | 
				
			||||||
 | 
					    pub fn from_raw_parts(reader: R, file: ZipFile) -> ZipFileReader<R> {
 | 
				
			||||||
 | 
					        ZipFileReader { reader, file }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns this ZIP file's information.
 | 
				
			||||||
 | 
					    pub fn file(&self) -> &ZipFile {
 | 
				
			||||||
 | 
					        &self.file
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a mutable reference to the inner seekable source.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Swapping the source (eg. via std::mem operations) may lead to inaccurate parsing.
 | 
				
			||||||
 | 
					    pub fn inner_mut(&mut self) -> &mut R {
 | 
				
			||||||
 | 
					        &mut self.reader
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the inner seekable source by consuming self.
 | 
				
			||||||
 | 
					    pub fn into_inner(self) -> R {
 | 
				
			||||||
 | 
					        self.reader
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a new entry reader if the provided index is valid.
 | 
				
			||||||
 | 
					    pub async fn reader_without_entry(&mut self, index: usize) -> Result<ZipEntryReader<'_, R, WithoutEntry>> {
 | 
				
			||||||
 | 
					        let stored_entry = self.file.entries.get(index).ok_or(ZipError::EntryIndexOutOfBounds)?;
 | 
				
			||||||
 | 
					        stored_entry.seek_to_data_offset(&mut self.reader).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(ZipEntryReader::new_with_borrow(
 | 
				
			||||||
 | 
					            &mut self.reader,
 | 
				
			||||||
 | 
					            stored_entry.entry.compression(),
 | 
				
			||||||
 | 
					            stored_entry.entry.compressed_size(),
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a new entry reader if the provided index is valid.
 | 
				
			||||||
 | 
					    pub async fn reader_with_entry(&mut self, index: usize) -> Result<ZipEntryReader<'_, R, WithEntry<'_>>> {
 | 
				
			||||||
 | 
					        let stored_entry = self.file.entries.get(index).ok_or(ZipError::EntryIndexOutOfBounds)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stored_entry.seek_to_data_offset(&mut self.reader).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reader = ZipEntryReader::new_with_borrow(
 | 
				
			||||||
 | 
					            &mut self.reader,
 | 
				
			||||||
 | 
					            stored_entry.entry.compression(),
 | 
				
			||||||
 | 
					            stored_entry.entry.compressed_size(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(reader.into_with_entry(stored_entry))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a new entry reader if the provided index is valid.
 | 
				
			||||||
 | 
					    /// Consumes self
 | 
				
			||||||
 | 
					    pub async fn into_entry<'a>(mut self, index: usize) -> Result<ZipEntryReader<'a, R, WithoutEntry>>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        R: 'a,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let stored_entry = self.file.entries.get(index).ok_or(ZipError::EntryIndexOutOfBounds)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stored_entry.seek_to_data_offset(&mut self.reader).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(ZipEntryReader::new_with_owned(
 | 
				
			||||||
 | 
					            self.reader,
 | 
				
			||||||
 | 
					            stored_entry.entry.compression(),
 | 
				
			||||||
 | 
					            stored_entry.entry.compressed_size(),
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					impl<R> ZipFileReader<Compat<R>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: tokio::io::AsyncBufRead + tokio::io::AsyncSeek + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new tokio-specific ZIP reader from a seekable source.
 | 
				
			||||||
 | 
					    pub async fn with_tokio(reader: R) -> Result<TokioZipFileReader<R>> {
 | 
				
			||||||
 | 
					        let mut reader = reader.compat();
 | 
				
			||||||
 | 
					        let file = crate::base::read::file(&mut reader).await?;
 | 
				
			||||||
 | 
					        Ok(ZipFileReader::from_raw_parts(reader, file))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										174
									
								
								crates/async_zip/src/base/read/stream.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								crates/async_zip/src/base/read/stream.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,174 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A ZIP reader which acts over a non-seekable source.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! # API Design
 | 
				
			||||||
 | 
					//! As opposed to other readers provided by this crate, it's important that the data of an entry is fully read before
 | 
				
			||||||
 | 
					//! the proceeding entry is read. This is as a result of not being able to seek forwards or backwards, so we must end
 | 
				
			||||||
 | 
					//! up at the start of the next entry.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! **We encode this invariant within Rust's type system so that it can be enforced at compile time.**
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! This requires that any transition methods between these encoded types consume the reader and provide a new owned
 | 
				
			||||||
 | 
					//! reader back. This is certainly something to keep in mind when working with this reader, but idiomatic code can
 | 
				
			||||||
 | 
					//! still be produced nevertheless.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! # Considerations
 | 
				
			||||||
 | 
					//! As the central directory of a ZIP archive is stored at the end of it, a non-seekable reader doesn't have access
 | 
				
			||||||
 | 
					//! to it. We have to rely on information provided within the local file header which may not be accurate or complete.
 | 
				
			||||||
 | 
					//! This results in:
 | 
				
			||||||
 | 
					//! - The inability to read ZIP entries using the combination of a data descriptor and the Stored compression method.
 | 
				
			||||||
 | 
					//! - No file comment being available (defaults to an empty string).
 | 
				
			||||||
 | 
					//! - No internal or external file attributes being available (defaults to 0).
 | 
				
			||||||
 | 
					//! - The extra field data potentially being inconsistent with what's stored in the central directory.
 | 
				
			||||||
 | 
					//! - None of the following being available when the entry was written with a data descriptor (defaults to 0):
 | 
				
			||||||
 | 
					//!     - CRC
 | 
				
			||||||
 | 
					//!     - compressed size
 | 
				
			||||||
 | 
					//!     - uncompressed size
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! # Example
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # use futures_lite::io::Cursor;
 | 
				
			||||||
 | 
					//! # use async_zip::error::Result;
 | 
				
			||||||
 | 
					//! # use async_zip::base::read::stream::ZipFileReader;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! # async fn run() -> Result<()> {
 | 
				
			||||||
 | 
					//! let mut zip = ZipFileReader::new(Cursor::new([0; 0]));
 | 
				
			||||||
 | 
					//!     
 | 
				
			||||||
 | 
					//! // Print the name of every file in a ZIP archive.
 | 
				
			||||||
 | 
					//! while let Some(entry) = zip.next_with_entry().await? {
 | 
				
			||||||
 | 
					//!     println!("File: {}", entry.reader().entry().filename().as_str().unwrap());
 | 
				
			||||||
 | 
					//!     zip = entry.skip().await?;
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! #     Ok(())
 | 
				
			||||||
 | 
					//! # }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::io::ConsumeDataDescriptor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::entry::ZipEntryReader;
 | 
				
			||||||
 | 
					use crate::error::Result;
 | 
				
			||||||
 | 
					use crate::error::ZipError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					use crate::tokio::read::stream::Ready as TokioReady;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncBufRead;
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					use tokio_util::compat::TokioAsyncReadCompatExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::io::entry::WithEntry;
 | 
				
			||||||
 | 
					use super::io::entry::WithoutEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type which encodes that [`ZipFileReader`] is ready to open a new entry.
 | 
				
			||||||
 | 
					pub struct Ready<R>(R);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type which encodes that [`ZipFileReader`] is currently reading an entry.
 | 
				
			||||||
 | 
					pub struct Reading<'a, R, E>(ZipEntryReader<'a, R, E>, bool);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A ZIP reader which acts over a non-seekable source.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// See the [module-level docs](.) for more information.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct ZipFileReader<S>(S);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R> ZipFileReader<Ready<R>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin + 'a,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new ZIP reader from a non-seekable source.
 | 
				
			||||||
 | 
					    pub fn new(reader: R) -> Self {
 | 
				
			||||||
 | 
					        Self(Ready(reader))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Opens the next entry for reading if the central directory hasn’t yet been reached.
 | 
				
			||||||
 | 
					    pub async fn next_without_entry(mut self) -> Result<Option<ZipFileReader<Reading<'a, R, WithoutEntry>>>> {
 | 
				
			||||||
 | 
					        let entry = match crate::base::read::lfh(&mut self.0 .0).await? {
 | 
				
			||||||
 | 
					            Some(entry) => entry,
 | 
				
			||||||
 | 
					            None => return Ok(None),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let length = if entry.data_descriptor { u64::MAX } else { entry.compressed_size };
 | 
				
			||||||
 | 
					        let reader = ZipEntryReader::new_with_owned(self.0 .0, entry.compression, length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(Some(ZipFileReader(Reading(reader, entry.data_descriptor))))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Opens the next entry for reading if the central directory hasn’t yet been reached.
 | 
				
			||||||
 | 
					    pub async fn next_with_entry(mut self) -> Result<Option<ZipFileReader<Reading<'a, R, WithEntry<'a>>>>> {
 | 
				
			||||||
 | 
					        let entry = match crate::base::read::lfh(&mut self.0 .0).await? {
 | 
				
			||||||
 | 
					            Some(entry) => entry,
 | 
				
			||||||
 | 
					            None => return Ok(None),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let length = if entry.data_descriptor { u64::MAX } else { entry.compressed_size };
 | 
				
			||||||
 | 
					        let reader = ZipEntryReader::new_with_owned(self.0 .0, entry.compression, length);
 | 
				
			||||||
 | 
					        let data_descriptor = entry.data_descriptor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(Some(ZipFileReader(Reading(reader.into_with_entry_owned(entry), data_descriptor))))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes the `ZipFileReader` returning the original `reader`
 | 
				
			||||||
 | 
					    pub async fn into_inner(self) -> R {
 | 
				
			||||||
 | 
					        self.0 .0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					impl<R> ZipFileReader<TokioReady<R>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: tokio::io::AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new tokio-specific ZIP reader from a non-seekable source.
 | 
				
			||||||
 | 
					    pub fn with_tokio(reader: R) -> ZipFileReader<TokioReady<R>> {
 | 
				
			||||||
 | 
					        Self(Ready(reader.compat()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, R, E> ZipFileReader<Reading<'a, R, E>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    R: AsyncBufRead + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Returns an immutable reference to the inner entry reader.
 | 
				
			||||||
 | 
					    pub fn reader(&self) -> &ZipEntryReader<'a, R, E> {
 | 
				
			||||||
 | 
					        &self.0 .0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a mutable reference to the inner entry reader.
 | 
				
			||||||
 | 
					    pub fn reader_mut(&mut self) -> &mut ZipEntryReader<'a, R, E> {
 | 
				
			||||||
 | 
					        &mut self.0 .0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Converts the reader back into the Ready state if EOF has been reached.
 | 
				
			||||||
 | 
					    pub async fn done(mut self) -> Result<ZipFileReader<Ready<R>>> {
 | 
				
			||||||
 | 
					        if self.0 .0.read(&mut [0; 1]).await? != 0 {
 | 
				
			||||||
 | 
					            return Err(ZipError::EOFNotReached);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut inner = self.0 .0.into_inner();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Has data descriptor.
 | 
				
			||||||
 | 
					        if self.0 .1 {
 | 
				
			||||||
 | 
					            ConsumeDataDescriptor(&mut inner).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(ZipFileReader(Ready(inner)))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Reads until EOF and converts the reader back into the Ready state.
 | 
				
			||||||
 | 
					    pub async fn skip(mut self) -> Result<ZipFileReader<Ready<R>>> {
 | 
				
			||||||
 | 
					        while self.0 .0.read(&mut [0; 2048]).await? != 0 {}
 | 
				
			||||||
 | 
					        let mut inner = self.0 .0.into_inner();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Has data descriptor.
 | 
				
			||||||
 | 
					        if self.0 .1 {
 | 
				
			||||||
 | 
					            ConsumeDataDescriptor(&mut inner).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(ZipFileReader(Ready(inner)))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										137
									
								
								crates/async_zip/src/base/write/compressed_writer.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								crates/async_zip/src/base/write/compressed_writer.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,137 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::write::io::offset::AsyncOffsetWriter;
 | 
				
			||||||
 | 
					use crate::spec::Compression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::io::Error;
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(any(feature = "deflate", feature = "bzip2", feature = "zstd", feature = "lzma", feature = "xz"))]
 | 
				
			||||||
 | 
					use async_compression::futures::write;
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncWrite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum CompressedAsyncWriter<'b, W: AsyncWrite + Unpin> {
 | 
				
			||||||
 | 
					    Stored(ShutdownIgnoredWriter<&'b mut AsyncOffsetWriter<W>>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					    Deflate(write::DeflateEncoder<ShutdownIgnoredWriter<&'b mut AsyncOffsetWriter<W>>>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					    Bz(write::BzEncoder<ShutdownIgnoredWriter<&'b mut AsyncOffsetWriter<W>>>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					    Lzma(write::LzmaEncoder<ShutdownIgnoredWriter<&'b mut AsyncOffsetWriter<W>>>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					    Zstd(write::ZstdEncoder<ShutdownIgnoredWriter<&'b mut AsyncOffsetWriter<W>>>),
 | 
				
			||||||
 | 
					    #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					    Xz(write::XzEncoder<ShutdownIgnoredWriter<&'b mut AsyncOffsetWriter<W>>>),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'b, W: AsyncWrite + Unpin> CompressedAsyncWriter<'b, W> {
 | 
				
			||||||
 | 
					    pub fn from_raw(writer: &'b mut AsyncOffsetWriter<W>, compression: Compression) -> Self {
 | 
				
			||||||
 | 
					        match compression {
 | 
				
			||||||
 | 
					            Compression::Stored => CompressedAsyncWriter::Stored(ShutdownIgnoredWriter(writer)),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            Compression::Deflate => {
 | 
				
			||||||
 | 
					                CompressedAsyncWriter::Deflate(write::DeflateEncoder::new(ShutdownIgnoredWriter(writer)))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					            Compression::Deflate64 => panic!("writing deflate64 is not supported"),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            Compression::Bz => CompressedAsyncWriter::Bz(write::BzEncoder::new(ShutdownIgnoredWriter(writer))),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            Compression::Lzma => CompressedAsyncWriter::Lzma(write::LzmaEncoder::new(ShutdownIgnoredWriter(writer))),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            Compression::Zstd => CompressedAsyncWriter::Zstd(write::ZstdEncoder::new(ShutdownIgnoredWriter(writer))),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            Compression::Xz => CompressedAsyncWriter::Xz(write::XzEncoder::new(ShutdownIgnoredWriter(writer))),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn into_inner(self) -> &'b mut AsyncOffsetWriter<W> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Stored(inner) => inner.into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Deflate(inner) => inner.into_inner().into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Bz(inner) => inner.into_inner().into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Lzma(inner) => inner.into_inner().into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Zstd(inner) => inner.into_inner().into_inner(),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Xz(inner) => inner.into_inner().into_inner(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'b, W: AsyncWrite + Unpin> AsyncWrite for CompressedAsyncWriter<'b, W> {
 | 
				
			||||||
 | 
					    fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<std::result::Result<usize, Error>> {
 | 
				
			||||||
 | 
					        match *self {
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Stored(ref mut inner) => Pin::new(inner).poll_write(cx, buf),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Deflate(ref mut inner) => Pin::new(inner).poll_write(cx, buf),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Bz(ref mut inner) => Pin::new(inner).poll_write(cx, buf),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Lzma(ref mut inner) => Pin::new(inner).poll_write(cx, buf),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Zstd(ref mut inner) => Pin::new(inner).poll_write(cx, buf),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Xz(ref mut inner) => Pin::new(inner).poll_write(cx, buf),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<std::result::Result<(), Error>> {
 | 
				
			||||||
 | 
					        match *self {
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Stored(ref mut inner) => Pin::new(inner).poll_flush(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Deflate(ref mut inner) => Pin::new(inner).poll_flush(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Bz(ref mut inner) => Pin::new(inner).poll_flush(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Lzma(ref mut inner) => Pin::new(inner).poll_flush(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Zstd(ref mut inner) => Pin::new(inner).poll_flush(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Xz(ref mut inner) => Pin::new(inner).poll_flush(cx),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<std::result::Result<(), Error>> {
 | 
				
			||||||
 | 
					        match *self {
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Stored(ref mut inner) => Pin::new(inner).poll_close(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Deflate(ref mut inner) => Pin::new(inner).poll_close(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Bz(ref mut inner) => Pin::new(inner).poll_close(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Lzma(ref mut inner) => Pin::new(inner).poll_close(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Zstd(ref mut inner) => Pin::new(inner).poll_close(cx),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            CompressedAsyncWriter::Xz(ref mut inner) => Pin::new(inner).poll_close(cx),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ShutdownIgnoredWriter<W: AsyncWrite + Unpin>(W);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<W: AsyncWrite + Unpin> ShutdownIgnoredWriter<W> {
 | 
				
			||||||
 | 
					    pub fn into_inner(self) -> W {
 | 
				
			||||||
 | 
					        self.0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<W: AsyncWrite + Unpin> AsyncWrite for ShutdownIgnoredWriter<W> {
 | 
				
			||||||
 | 
					    fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<std::result::Result<usize, Error>> {
 | 
				
			||||||
 | 
					        Pin::new(&mut self.0).poll_write(cx, buf)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<std::result::Result<(), Error>> {
 | 
				
			||||||
 | 
					        Pin::new(&mut self.0).poll_flush(cx)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_close(self: Pin<&mut Self>, _: &mut Context) -> Poll<std::result::Result<(), Error>> {
 | 
				
			||||||
 | 
					        Poll::Ready(Ok(()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										272
									
								
								crates/async_zip/src/base/write/entry_stream.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								crates/async_zip/src/base/write/entry_stream.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,272 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::write::compressed_writer::CompressedAsyncWriter;
 | 
				
			||||||
 | 
					use crate::base::write::get_or_put_info_zip_unicode_comment_extra_field_mut;
 | 
				
			||||||
 | 
					use crate::base::write::get_or_put_info_zip_unicode_path_extra_field_mut;
 | 
				
			||||||
 | 
					use crate::base::write::io::offset::AsyncOffsetWriter;
 | 
				
			||||||
 | 
					use crate::base::write::CentralDirectoryEntry;
 | 
				
			||||||
 | 
					use crate::base::write::ZipFileWriter;
 | 
				
			||||||
 | 
					use crate::entry::ZipEntry;
 | 
				
			||||||
 | 
					use crate::error::{Result, Zip64ErrorCase, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::extra_field::ExtraFieldAsBytes;
 | 
				
			||||||
 | 
					use crate::spec::header::InfoZipUnicodeCommentExtraField;
 | 
				
			||||||
 | 
					use crate::spec::header::InfoZipUnicodePathExtraField;
 | 
				
			||||||
 | 
					use crate::spec::header::{
 | 
				
			||||||
 | 
					    CentralDirectoryRecord, ExtraField, GeneralPurposeFlag, HeaderId, LocalFileHeader,
 | 
				
			||||||
 | 
					    Zip64ExtendedInformationExtraField,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::string::StringEncoding;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::io::Error;
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::get_zip64_extra_field_mut;
 | 
				
			||||||
 | 
					use crate::spec::consts::{NON_ZIP64_MAX_NUM_FILES, NON_ZIP64_MAX_SIZE};
 | 
				
			||||||
 | 
					use crc32fast::Hasher;
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncWrite, AsyncWriteExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An entry writer which supports the streaming of data (ie. the writing of unknown size or data at runtime).
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// # Note
 | 
				
			||||||
 | 
					/// - This writer cannot be manually constructed; instead, use [`ZipFileWriter::write_entry_stream()`].
 | 
				
			||||||
 | 
					/// - [`EntryStreamWriter::close()`] must be called before a stream writer goes out of scope.
 | 
				
			||||||
 | 
					/// - Utilities for working with [`AsyncWrite`] values are provided by [`AsyncWriteExt`].
 | 
				
			||||||
 | 
					pub struct EntryStreamWriter<'b, W: AsyncWrite + Unpin> {
 | 
				
			||||||
 | 
					    writer: AsyncOffsetWriter<CompressedAsyncWriter<'b, W>>,
 | 
				
			||||||
 | 
					    cd_entries: &'b mut Vec<CentralDirectoryEntry>,
 | 
				
			||||||
 | 
					    entry: ZipEntry,
 | 
				
			||||||
 | 
					    hasher: Hasher,
 | 
				
			||||||
 | 
					    lfh: LocalFileHeader,
 | 
				
			||||||
 | 
					    lfh_offset: u64,
 | 
				
			||||||
 | 
					    data_offset: u64,
 | 
				
			||||||
 | 
					    force_no_zip64: bool,
 | 
				
			||||||
 | 
					    /// To write back to the original writer if zip64 is required.
 | 
				
			||||||
 | 
					    is_zip64: &'b mut bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'b, W: AsyncWrite + Unpin> EntryStreamWriter<'b, W> {
 | 
				
			||||||
 | 
					    pub(crate) async fn from_raw(
 | 
				
			||||||
 | 
					        writer: &'b mut ZipFileWriter<W>,
 | 
				
			||||||
 | 
					        mut entry: ZipEntry,
 | 
				
			||||||
 | 
					    ) -> Result<EntryStreamWriter<'b, W>> {
 | 
				
			||||||
 | 
					        let lfh_offset = writer.writer.offset();
 | 
				
			||||||
 | 
					        let lfh = EntryStreamWriter::write_lfh(writer, &mut entry).await?;
 | 
				
			||||||
 | 
					        let data_offset = writer.writer.offset();
 | 
				
			||||||
 | 
					        let force_no_zip64 = writer.force_no_zip64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cd_entries = &mut writer.cd_entries;
 | 
				
			||||||
 | 
					        let is_zip64 = &mut writer.is_zip64;
 | 
				
			||||||
 | 
					        let writer = AsyncOffsetWriter::new(CompressedAsyncWriter::from_raw(&mut writer.writer, entry.compression()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(EntryStreamWriter {
 | 
				
			||||||
 | 
					            writer,
 | 
				
			||||||
 | 
					            cd_entries,
 | 
				
			||||||
 | 
					            entry,
 | 
				
			||||||
 | 
					            lfh,
 | 
				
			||||||
 | 
					            lfh_offset,
 | 
				
			||||||
 | 
					            data_offset,
 | 
				
			||||||
 | 
					            hasher: Hasher::new(),
 | 
				
			||||||
 | 
					            force_no_zip64,
 | 
				
			||||||
 | 
					            is_zip64,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn write_lfh(writer: &'b mut ZipFileWriter<W>, entry: &mut ZipEntry) -> Result<LocalFileHeader> {
 | 
				
			||||||
 | 
					        // Always emit a zip64 extended field, even if we don't need it, because we *might* need it.
 | 
				
			||||||
 | 
					        // If we are forcing no zip, we will have to error later if the file is too large.
 | 
				
			||||||
 | 
					        let (lfh_compressed, lfh_uncompressed) = if !writer.force_no_zip64 {
 | 
				
			||||||
 | 
					            if !writer.is_zip64 {
 | 
				
			||||||
 | 
					                writer.is_zip64 = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            entry.extra_fields.push(ExtraField::Zip64ExtendedInformation(Zip64ExtendedInformationExtraField {
 | 
				
			||||||
 | 
					                header_id: HeaderId::ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD,
 | 
				
			||||||
 | 
					                uncompressed_size: Some(entry.uncompressed_size),
 | 
				
			||||||
 | 
					                compressed_size: Some(entry.compressed_size),
 | 
				
			||||||
 | 
					                relative_header_offset: None,
 | 
				
			||||||
 | 
					                disk_start_number: None,
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            (NON_ZIP64_MAX_SIZE, NON_ZIP64_MAX_SIZE)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            if entry.compressed_size > NON_ZIP64_MAX_SIZE as u64 || entry.uncompressed_size > NON_ZIP64_MAX_SIZE as u64
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return Err(ZipError::Zip64Needed(Zip64ErrorCase::LargeFile));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            (entry.compressed_size as u32, entry.uncompressed_size as u32)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let utf8_without_alternative =
 | 
				
			||||||
 | 
					            entry.filename().is_utf8_without_alternative() && entry.comment().is_utf8_without_alternative();
 | 
				
			||||||
 | 
					        if !utf8_without_alternative {
 | 
				
			||||||
 | 
					            if matches!(entry.filename().encoding(), StringEncoding::Utf8) {
 | 
				
			||||||
 | 
					                let u_file_name = entry.filename().as_bytes().to_vec();
 | 
				
			||||||
 | 
					                if !u_file_name.is_empty() {
 | 
				
			||||||
 | 
					                    let basic_crc32 =
 | 
				
			||||||
 | 
					                        crc32fast::hash(entry.filename().alternative().unwrap_or_else(|| entry.filename().as_bytes()));
 | 
				
			||||||
 | 
					                    let upath_field = get_or_put_info_zip_unicode_path_extra_field_mut(entry.extra_fields.as_mut());
 | 
				
			||||||
 | 
					                    if let InfoZipUnicodePathExtraField::V1 { crc32, unicode } = upath_field {
 | 
				
			||||||
 | 
					                        *crc32 = basic_crc32;
 | 
				
			||||||
 | 
					                        *unicode = u_file_name;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if matches!(entry.comment().encoding(), StringEncoding::Utf8) {
 | 
				
			||||||
 | 
					                let u_comment = entry.comment().as_bytes().to_vec();
 | 
				
			||||||
 | 
					                if !u_comment.is_empty() {
 | 
				
			||||||
 | 
					                    let basic_crc32 =
 | 
				
			||||||
 | 
					                        crc32fast::hash(entry.comment().alternative().unwrap_or_else(|| entry.comment().as_bytes()));
 | 
				
			||||||
 | 
					                    let ucom_field = get_or_put_info_zip_unicode_comment_extra_field_mut(entry.extra_fields.as_mut());
 | 
				
			||||||
 | 
					                    if let InfoZipUnicodeCommentExtraField::V1 { crc32, unicode } = ucom_field {
 | 
				
			||||||
 | 
					                        *crc32 = basic_crc32;
 | 
				
			||||||
 | 
					                        *unicode = u_comment;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let filename_basic = entry.filename().alternative().unwrap_or_else(|| entry.filename().as_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let lfh = LocalFileHeader {
 | 
				
			||||||
 | 
					            compressed_size: lfh_compressed,
 | 
				
			||||||
 | 
					            uncompressed_size: lfh_uncompressed,
 | 
				
			||||||
 | 
					            compression: entry.compression().into(),
 | 
				
			||||||
 | 
					            crc: entry.crc32,
 | 
				
			||||||
 | 
					            extra_field_length: entry
 | 
				
			||||||
 | 
					                .extra_fields()
 | 
				
			||||||
 | 
					                .count_bytes()
 | 
				
			||||||
 | 
					                .try_into()
 | 
				
			||||||
 | 
					                .map_err(|_| ZipError::ExtraFieldTooLarge)?,
 | 
				
			||||||
 | 
					            file_name_length: filename_basic.len().try_into().map_err(|_| ZipError::FileNameTooLarge)?,
 | 
				
			||||||
 | 
					            mod_time: entry.last_modification_date().time,
 | 
				
			||||||
 | 
					            mod_date: entry.last_modification_date().date,
 | 
				
			||||||
 | 
					            version: crate::spec::version::as_needed_to_extract(entry),
 | 
				
			||||||
 | 
					            flags: GeneralPurposeFlag {
 | 
				
			||||||
 | 
					                data_descriptor: true,
 | 
				
			||||||
 | 
					                encrypted: false,
 | 
				
			||||||
 | 
					                filename_unicode: utf8_without_alternative,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        writer.writer.write_all(&crate::spec::consts::LFH_SIGNATURE.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					        writer.writer.write_all(&lfh.as_slice()).await?;
 | 
				
			||||||
 | 
					        writer.writer.write_all(filename_basic).await?;
 | 
				
			||||||
 | 
					        writer.writer.write_all(&entry.extra_fields().as_bytes()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(lfh)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this entry writer and completes all closing tasks.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This includes:
 | 
				
			||||||
 | 
					    /// - Finalising the CRC32 hash value for the written data.
 | 
				
			||||||
 | 
					    /// - Calculating the compressed and uncompressed byte sizes.
 | 
				
			||||||
 | 
					    /// - Constructing a central directory header.
 | 
				
			||||||
 | 
					    /// - Pushing that central directory header to the [`ZipFileWriter`]'s store.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Failure to call this function before going out of scope would result in a corrupted ZIP file.
 | 
				
			||||||
 | 
					    pub async fn close(mut self) -> Result<()> {
 | 
				
			||||||
 | 
					        self.writer.close().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let crc = self.hasher.finalize();
 | 
				
			||||||
 | 
					        let uncompressed_size = self.writer.offset();
 | 
				
			||||||
 | 
					        let inner_writer = self.writer.into_inner().into_inner();
 | 
				
			||||||
 | 
					        let compressed_size = inner_writer.offset() - self.data_offset;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (cdr_compressed_size, cdr_uncompressed_size, lh_offset) = if self.force_no_zip64 {
 | 
				
			||||||
 | 
					            if uncompressed_size > NON_ZIP64_MAX_SIZE as u64
 | 
				
			||||||
 | 
					                || compressed_size > NON_ZIP64_MAX_SIZE as u64
 | 
				
			||||||
 | 
					                || self.lfh_offset > NON_ZIP64_MAX_SIZE as u64
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return Err(ZipError::Zip64Needed(Zip64ErrorCase::LargeFile));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            (uncompressed_size as u32, compressed_size as u32, self.lfh_offset as u32)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // When streaming an entry, we are always using a zip64 field.
 | 
				
			||||||
 | 
					            match get_zip64_extra_field_mut(&mut self.entry.extra_fields) {
 | 
				
			||||||
 | 
					                // This case shouldn't be necessary but is included for completeness.
 | 
				
			||||||
 | 
					                None => {
 | 
				
			||||||
 | 
					                    self.entry.extra_fields.push(ExtraField::Zip64ExtendedInformation(
 | 
				
			||||||
 | 
					                        Zip64ExtendedInformationExtraField {
 | 
				
			||||||
 | 
					                            header_id: HeaderId::ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD,
 | 
				
			||||||
 | 
					                            uncompressed_size: Some(uncompressed_size),
 | 
				
			||||||
 | 
					                            compressed_size: Some(compressed_size),
 | 
				
			||||||
 | 
					                            relative_header_offset: Some(self.lfh_offset),
 | 
				
			||||||
 | 
					                            disk_start_number: None,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Some(zip64) => {
 | 
				
			||||||
 | 
					                    zip64.uncompressed_size = Some(uncompressed_size);
 | 
				
			||||||
 | 
					                    zip64.compressed_size = Some(compressed_size);
 | 
				
			||||||
 | 
					                    zip64.relative_header_offset = Some(self.lfh_offset);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            self.lfh.extra_field_length =
 | 
				
			||||||
 | 
					                self.entry.extra_fields().count_bytes().try_into().map_err(|_| ZipError::ExtraFieldTooLarge)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            (NON_ZIP64_MAX_SIZE, NON_ZIP64_MAX_SIZE, NON_ZIP64_MAX_SIZE)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        inner_writer.write_all(&crate::spec::consts::DATA_DESCRIPTOR_SIGNATURE.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					        inner_writer.write_all(&crc.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					        inner_writer.write_all(&cdr_compressed_size.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					        inner_writer.write_all(&cdr_uncompressed_size.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let comment_basic = self.entry.comment().alternative().unwrap_or_else(|| self.entry.comment().as_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let cdh = CentralDirectoryRecord {
 | 
				
			||||||
 | 
					            compressed_size: cdr_compressed_size,
 | 
				
			||||||
 | 
					            uncompressed_size: cdr_uncompressed_size,
 | 
				
			||||||
 | 
					            crc,
 | 
				
			||||||
 | 
					            v_made_by: crate::spec::version::as_made_by(),
 | 
				
			||||||
 | 
					            v_needed: self.lfh.version,
 | 
				
			||||||
 | 
					            compression: self.lfh.compression,
 | 
				
			||||||
 | 
					            extra_field_length: self.lfh.extra_field_length,
 | 
				
			||||||
 | 
					            file_name_length: self.lfh.file_name_length,
 | 
				
			||||||
 | 
					            file_comment_length: comment_basic.len().try_into().map_err(|_| ZipError::CommentTooLarge)?,
 | 
				
			||||||
 | 
					            mod_time: self.lfh.mod_time,
 | 
				
			||||||
 | 
					            mod_date: self.lfh.mod_date,
 | 
				
			||||||
 | 
					            flags: self.lfh.flags,
 | 
				
			||||||
 | 
					            disk_start: 0,
 | 
				
			||||||
 | 
					            inter_attr: self.entry.internal_file_attribute(),
 | 
				
			||||||
 | 
					            exter_attr: self.entry.external_file_attribute(),
 | 
				
			||||||
 | 
					            lh_offset,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.cd_entries.push(CentralDirectoryEntry { header: cdh, entry: self.entry });
 | 
				
			||||||
 | 
					        // Ensure that we can fit this many files in this archive if forcing no zip64
 | 
				
			||||||
 | 
					        if self.cd_entries.len() > NON_ZIP64_MAX_NUM_FILES as usize {
 | 
				
			||||||
 | 
					            if self.force_no_zip64 {
 | 
				
			||||||
 | 
					                return Err(ZipError::Zip64Needed(Zip64ErrorCase::TooManyFiles));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if !*self.is_zip64 {
 | 
				
			||||||
 | 
					                *self.is_zip64 = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a, W: AsyncWrite + Unpin> AsyncWrite for EntryStreamWriter<'a, W> {
 | 
				
			||||||
 | 
					    fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<std::result::Result<usize, Error>> {
 | 
				
			||||||
 | 
					        let poll = Pin::new(&mut self.writer).poll_write(cx, buf);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Poll::Ready(Ok(written)) = poll {
 | 
				
			||||||
 | 
					            self.hasher.update(&buf[0..written]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        poll
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<std::result::Result<(), Error>> {
 | 
				
			||||||
 | 
					        Pin::new(&mut self.writer).poll_flush(cx)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<std::result::Result<(), Error>> {
 | 
				
			||||||
 | 
					        Pin::new(&mut self.writer).poll_close(cx)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										259
									
								
								crates/async_zip/src/base/write/entry_whole.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								crates/async_zip/src/base/write/entry_whole.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,259 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::write::get_or_put_info_zip_unicode_comment_extra_field_mut;
 | 
				
			||||||
 | 
					use crate::base::write::get_or_put_info_zip_unicode_path_extra_field_mut;
 | 
				
			||||||
 | 
					use crate::base::write::{CentralDirectoryEntry, ZipFileWriter};
 | 
				
			||||||
 | 
					use crate::entry::ZipEntry;
 | 
				
			||||||
 | 
					use crate::error::{Result, Zip64ErrorCase, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::extra_field::Zip64ExtendedInformationExtraFieldBuilder;
 | 
				
			||||||
 | 
					use crate::spec::header::{InfoZipUnicodeCommentExtraField, InfoZipUnicodePathExtraField};
 | 
				
			||||||
 | 
					use crate::spec::{
 | 
				
			||||||
 | 
					    extra_field::ExtraFieldAsBytes,
 | 
				
			||||||
 | 
					    header::{CentralDirectoryRecord, ExtraField, GeneralPurposeFlag, LocalFileHeader},
 | 
				
			||||||
 | 
					    Compression,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::StringEncoding;
 | 
				
			||||||
 | 
					#[cfg(any(feature = "deflate", feature = "bzip2", feature = "zstd", feature = "lzma", feature = "xz"))]
 | 
				
			||||||
 | 
					use futures_lite::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::spec::consts::{NON_ZIP64_MAX_NUM_FILES, NON_ZIP64_MAX_SIZE};
 | 
				
			||||||
 | 
					#[cfg(any(feature = "deflate", feature = "bzip2", feature = "zstd", feature = "lzma", feature = "xz"))]
 | 
				
			||||||
 | 
					use async_compression::futures::write;
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncWrite, AsyncWriteExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct EntryWholeWriter<'b, 'c, W: AsyncWrite + Unpin> {
 | 
				
			||||||
 | 
					    writer: &'b mut ZipFileWriter<W>,
 | 
				
			||||||
 | 
					    entry: ZipEntry,
 | 
				
			||||||
 | 
					    data: &'c [u8],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'b, 'c, W: AsyncWrite + Unpin> EntryWholeWriter<'b, 'c, W> {
 | 
				
			||||||
 | 
					    pub fn from_raw(writer: &'b mut ZipFileWriter<W>, entry: ZipEntry, data: &'c [u8]) -> Self {
 | 
				
			||||||
 | 
					        Self { writer, entry, data }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn write(mut self) -> Result<()> {
 | 
				
			||||||
 | 
					        let mut _compressed_data: Option<Vec<u8>> = None;
 | 
				
			||||||
 | 
					        let compressed_data = match self.entry.compression() {
 | 
				
			||||||
 | 
					            Compression::Stored => self.data,
 | 
				
			||||||
 | 
					            #[cfg(any(
 | 
				
			||||||
 | 
					                feature = "deflate",
 | 
				
			||||||
 | 
					                feature = "bzip2",
 | 
				
			||||||
 | 
					                feature = "zstd",
 | 
				
			||||||
 | 
					                feature = "lzma",
 | 
				
			||||||
 | 
					                feature = "xz",
 | 
				
			||||||
 | 
					                feature = "deflate64"
 | 
				
			||||||
 | 
					            ))]
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                _compressed_data =
 | 
				
			||||||
 | 
					                    Some(compress(self.entry.compression(), self.data, self.entry.compression_level).await);
 | 
				
			||||||
 | 
					                _compressed_data.as_ref().unwrap()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut zip64_extra_field_builder = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (lfh_uncompressed_size, lfh_compressed_size) = if self.data.len() as u64 > NON_ZIP64_MAX_SIZE as u64
 | 
				
			||||||
 | 
					            || compressed_data.len() as u64 > NON_ZIP64_MAX_SIZE as u64
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if self.writer.force_no_zip64 {
 | 
				
			||||||
 | 
					                return Err(ZipError::Zip64Needed(Zip64ErrorCase::LargeFile));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if !self.writer.is_zip64 {
 | 
				
			||||||
 | 
					                self.writer.is_zip64 = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            zip64_extra_field_builder = Some(
 | 
				
			||||||
 | 
					                Zip64ExtendedInformationExtraFieldBuilder::new()
 | 
				
			||||||
 | 
					                    .sizes(compressed_data.len() as u64, self.data.len() as u64),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            (NON_ZIP64_MAX_SIZE, NON_ZIP64_MAX_SIZE)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            (self.data.len() as u32, compressed_data.len() as u32)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let lh_offset = if self.writer.writer.offset() > NON_ZIP64_MAX_SIZE as u64 {
 | 
				
			||||||
 | 
					            if self.writer.force_no_zip64 {
 | 
				
			||||||
 | 
					                return Err(ZipError::Zip64Needed(Zip64ErrorCase::LargeFile));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if !self.writer.is_zip64 {
 | 
				
			||||||
 | 
					                self.writer.is_zip64 = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Some(zip64_extra_field) = zip64_extra_field_builder {
 | 
				
			||||||
 | 
					                zip64_extra_field_builder = Some(zip64_extra_field.relative_header_offset(self.writer.writer.offset()));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                zip64_extra_field_builder = Some(
 | 
				
			||||||
 | 
					                    Zip64ExtendedInformationExtraFieldBuilder::new()
 | 
				
			||||||
 | 
					                        .relative_header_offset(self.writer.writer.offset()),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            NON_ZIP64_MAX_SIZE
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            self.writer.writer.offset() as u32
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(builder) = zip64_extra_field_builder {
 | 
				
			||||||
 | 
					            if !builder.eof_only() {
 | 
				
			||||||
 | 
					                self.entry.extra_fields.push(ExtraField::Zip64ExtendedInformation(builder.build()?));
 | 
				
			||||||
 | 
					                zip64_extra_field_builder = None;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                zip64_extra_field_builder = Some(builder);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let utf8_without_alternative =
 | 
				
			||||||
 | 
					            self.entry.filename().is_utf8_without_alternative() && self.entry.comment().is_utf8_without_alternative();
 | 
				
			||||||
 | 
					        if !utf8_without_alternative {
 | 
				
			||||||
 | 
					            if matches!(self.entry.filename().encoding(), StringEncoding::Utf8) {
 | 
				
			||||||
 | 
					                let u_file_name = self.entry.filename().as_bytes().to_vec();
 | 
				
			||||||
 | 
					                if !u_file_name.is_empty() {
 | 
				
			||||||
 | 
					                    let basic_crc32 = crc32fast::hash(
 | 
				
			||||||
 | 
					                        self.entry.filename().alternative().unwrap_or_else(|| self.entry.filename().as_bytes()),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    let upath_field =
 | 
				
			||||||
 | 
					                        get_or_put_info_zip_unicode_path_extra_field_mut(self.entry.extra_fields.as_mut());
 | 
				
			||||||
 | 
					                    if let InfoZipUnicodePathExtraField::V1 { crc32, unicode } = upath_field {
 | 
				
			||||||
 | 
					                        *crc32 = basic_crc32;
 | 
				
			||||||
 | 
					                        *unicode = u_file_name;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if matches!(self.entry.comment().encoding(), StringEncoding::Utf8) {
 | 
				
			||||||
 | 
					                let u_comment = self.entry.comment().as_bytes().to_vec();
 | 
				
			||||||
 | 
					                if !u_comment.is_empty() {
 | 
				
			||||||
 | 
					                    let basic_crc32 = crc32fast::hash(
 | 
				
			||||||
 | 
					                        self.entry.comment().alternative().unwrap_or_else(|| self.entry.comment().as_bytes()),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    let ucom_field =
 | 
				
			||||||
 | 
					                        get_or_put_info_zip_unicode_comment_extra_field_mut(self.entry.extra_fields.as_mut());
 | 
				
			||||||
 | 
					                    if let InfoZipUnicodeCommentExtraField::V1 { crc32, unicode } = ucom_field {
 | 
				
			||||||
 | 
					                        *crc32 = basic_crc32;
 | 
				
			||||||
 | 
					                        *unicode = u_comment;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let filename_basic = self.entry.filename().alternative().unwrap_or_else(|| self.entry.filename().as_bytes());
 | 
				
			||||||
 | 
					        let comment_basic = self.entry.comment().alternative().unwrap_or_else(|| self.entry.comment().as_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let lf_header = LocalFileHeader {
 | 
				
			||||||
 | 
					            compressed_size: lfh_compressed_size,
 | 
				
			||||||
 | 
					            uncompressed_size: lfh_uncompressed_size,
 | 
				
			||||||
 | 
					            compression: self.entry.compression().into(),
 | 
				
			||||||
 | 
					            crc: crc32fast::hash(self.data),
 | 
				
			||||||
 | 
					            extra_field_length: self
 | 
				
			||||||
 | 
					                .entry
 | 
				
			||||||
 | 
					                .extra_fields()
 | 
				
			||||||
 | 
					                .count_bytes()
 | 
				
			||||||
 | 
					                .try_into()
 | 
				
			||||||
 | 
					                .map_err(|_| ZipError::ExtraFieldTooLarge)?,
 | 
				
			||||||
 | 
					            file_name_length: filename_basic.len().try_into().map_err(|_| ZipError::FileNameTooLarge)?,
 | 
				
			||||||
 | 
					            mod_time: self.entry.last_modification_date().time,
 | 
				
			||||||
 | 
					            mod_date: self.entry.last_modification_date().date,
 | 
				
			||||||
 | 
					            version: crate::spec::version::as_needed_to_extract(&self.entry),
 | 
				
			||||||
 | 
					            flags: GeneralPurposeFlag {
 | 
				
			||||||
 | 
					                data_descriptor: false,
 | 
				
			||||||
 | 
					                encrypted: false,
 | 
				
			||||||
 | 
					                filename_unicode: utf8_without_alternative,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut header = CentralDirectoryRecord {
 | 
				
			||||||
 | 
					            v_made_by: crate::spec::version::as_made_by(),
 | 
				
			||||||
 | 
					            v_needed: lf_header.version,
 | 
				
			||||||
 | 
					            compressed_size: lf_header.compressed_size,
 | 
				
			||||||
 | 
					            uncompressed_size: lf_header.uncompressed_size,
 | 
				
			||||||
 | 
					            compression: lf_header.compression,
 | 
				
			||||||
 | 
					            crc: lf_header.crc,
 | 
				
			||||||
 | 
					            extra_field_length: lf_header.extra_field_length,
 | 
				
			||||||
 | 
					            file_name_length: lf_header.file_name_length,
 | 
				
			||||||
 | 
					            file_comment_length: comment_basic.len().try_into().map_err(|_| ZipError::CommentTooLarge)?,
 | 
				
			||||||
 | 
					            mod_time: lf_header.mod_time,
 | 
				
			||||||
 | 
					            mod_date: lf_header.mod_date,
 | 
				
			||||||
 | 
					            flags: lf_header.flags,
 | 
				
			||||||
 | 
					            disk_start: 0,
 | 
				
			||||||
 | 
					            inter_attr: self.entry.internal_file_attribute(),
 | 
				
			||||||
 | 
					            exter_attr: self.entry.external_file_attribute(),
 | 
				
			||||||
 | 
					            lh_offset,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.writer.writer.write_all(&crate::spec::consts::LFH_SIGNATURE.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					        self.writer.writer.write_all(&lf_header.as_slice()).await?;
 | 
				
			||||||
 | 
					        self.writer.writer.write_all(filename_basic).await?;
 | 
				
			||||||
 | 
					        self.writer.writer.write_all(&self.entry.extra_fields().as_bytes()).await?;
 | 
				
			||||||
 | 
					        self.writer.writer.write_all(compressed_data).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(builder) = zip64_extra_field_builder {
 | 
				
			||||||
 | 
					            self.entry.extra_fields.push(ExtraField::Zip64ExtendedInformation(builder.build()?));
 | 
				
			||||||
 | 
					            header.extra_field_length =
 | 
				
			||||||
 | 
					                self.entry.extra_fields().count_bytes().try_into().map_err(|_| ZipError::ExtraFieldTooLarge)?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.writer.cd_entries.push(CentralDirectoryEntry { header, entry: self.entry });
 | 
				
			||||||
 | 
					        // Ensure that we can fit this many files in this archive if forcing no zip64
 | 
				
			||||||
 | 
					        if self.writer.cd_entries.len() > NON_ZIP64_MAX_NUM_FILES as usize {
 | 
				
			||||||
 | 
					            if self.writer.force_no_zip64 {
 | 
				
			||||||
 | 
					                return Err(ZipError::Zip64Needed(Zip64ErrorCase::TooManyFiles));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if !self.writer.is_zip64 {
 | 
				
			||||||
 | 
					                self.writer.is_zip64 = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(any(
 | 
				
			||||||
 | 
					    feature = "deflate",
 | 
				
			||||||
 | 
					    feature = "bzip2",
 | 
				
			||||||
 | 
					    feature = "zstd",
 | 
				
			||||||
 | 
					    feature = "lzma",
 | 
				
			||||||
 | 
					    feature = "xz",
 | 
				
			||||||
 | 
					    feature = "deflate64"
 | 
				
			||||||
 | 
					))]
 | 
				
			||||||
 | 
					async fn compress(compression: Compression, data: &[u8], level: async_compression::Level) -> Vec<u8> {
 | 
				
			||||||
 | 
					    // TODO: Reduce reallocations of Vec by making a lower-bound estimate of the length reduction and
 | 
				
			||||||
 | 
					    // pre-initialising the Vec to that length. Then truncate() to the actual number of bytes written.
 | 
				
			||||||
 | 
					    match compression {
 | 
				
			||||||
 | 
					        #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					        Compression::Deflate => {
 | 
				
			||||||
 | 
					            let mut writer = write::DeflateEncoder::with_quality(Cursor::new(Vec::new()), level);
 | 
				
			||||||
 | 
					            writer.write_all(data).await.unwrap();
 | 
				
			||||||
 | 
					            writer.close().await.unwrap();
 | 
				
			||||||
 | 
					            writer.into_inner().into_inner()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					        Compression::Deflate64 => panic!("compressing deflate64 is not supported"),
 | 
				
			||||||
 | 
					        #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					        Compression::Bz => {
 | 
				
			||||||
 | 
					            let mut writer = write::BzEncoder::with_quality(Cursor::new(Vec::new()), level);
 | 
				
			||||||
 | 
					            writer.write_all(data).await.unwrap();
 | 
				
			||||||
 | 
					            writer.close().await.unwrap();
 | 
				
			||||||
 | 
					            writer.into_inner().into_inner()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					        Compression::Lzma => {
 | 
				
			||||||
 | 
					            let mut writer = write::LzmaEncoder::with_quality(Cursor::new(Vec::new()), level);
 | 
				
			||||||
 | 
					            writer.write_all(data).await.unwrap();
 | 
				
			||||||
 | 
					            writer.close().await.unwrap();
 | 
				
			||||||
 | 
					            writer.into_inner().into_inner()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					        Compression::Xz => {
 | 
				
			||||||
 | 
					            let mut writer = write::XzEncoder::with_quality(Cursor::new(Vec::new()), level);
 | 
				
			||||||
 | 
					            writer.write_all(data).await.unwrap();
 | 
				
			||||||
 | 
					            writer.close().await.unwrap();
 | 
				
			||||||
 | 
					            writer.into_inner().into_inner()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					        Compression::Zstd => {
 | 
				
			||||||
 | 
					            let mut writer = write::ZstdEncoder::with_quality(Cursor::new(Vec::new()), level);
 | 
				
			||||||
 | 
					            writer.write_all(data).await.unwrap();
 | 
				
			||||||
 | 
					            writer.close().await.unwrap();
 | 
				
			||||||
 | 
					            writer.into_inner().into_inner()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _ => unreachable!(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								crates/async_zip/src/base/write/io/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								crates/async_zip/src/base/write/io/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod offset;
 | 
				
			||||||
							
								
								
									
										73
									
								
								crates/async_zip/src/base/write/io/offset.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								crates/async_zip/src/base/write/io/offset.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,73 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::io::{Error, IoSlice};
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncWrite;
 | 
				
			||||||
 | 
					use pin_project::pin_project;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A wrapper around an [`AsyncWrite`] implementation which tracks the current byte offset.
 | 
				
			||||||
 | 
					#[pin_project(project = OffsetWriterProj)]
 | 
				
			||||||
 | 
					pub struct AsyncOffsetWriter<W> {
 | 
				
			||||||
 | 
					    #[pin]
 | 
				
			||||||
 | 
					    inner: W,
 | 
				
			||||||
 | 
					    offset: u64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<W> AsyncOffsetWriter<W>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    W: AsyncWrite + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Constructs a new wrapper from an inner [`AsyncWrite`] writer.
 | 
				
			||||||
 | 
					    pub fn new(inner: W) -> Self {
 | 
				
			||||||
 | 
					        Self { inner, offset: 0 }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the current byte offset.
 | 
				
			||||||
 | 
					    pub fn offset(&self) -> u64 {
 | 
				
			||||||
 | 
					        self.offset
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this wrapper and returns the inner [`AsyncWrite`] writer.
 | 
				
			||||||
 | 
					    pub fn into_inner(self) -> W {
 | 
				
			||||||
 | 
					        self.inner
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn inner_mut(&mut self) -> &mut W {
 | 
				
			||||||
 | 
					        &mut self.inner
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<W> AsyncWrite for AsyncOffsetWriter<W>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    W: AsyncWrite + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<Result<usize, Error>> {
 | 
				
			||||||
 | 
					        let this = self.project();
 | 
				
			||||||
 | 
					        let poll = this.inner.poll_write(cx, buf);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Poll::Ready(Ok(inner)) = &poll {
 | 
				
			||||||
 | 
					            *this.offset += *inner as u64;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        poll
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Error>> {
 | 
				
			||||||
 | 
					        self.project().inner.poll_flush(cx)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Error>> {
 | 
				
			||||||
 | 
					        self.project().inner.poll_close(cx)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_write_vectored(
 | 
				
			||||||
 | 
					        self: Pin<&mut Self>,
 | 
				
			||||||
 | 
					        cx: &mut Context<'_>,
 | 
				
			||||||
 | 
					        bufs: &[IoSlice<'_>],
 | 
				
			||||||
 | 
					    ) -> Poll<Result<usize, Error>> {
 | 
				
			||||||
 | 
					        self.project().inner.poll_write_vectored(cx, bufs)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										290
									
								
								crates/async_zip/src/base/write/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								crates/async_zip/src/base/write/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,290 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021-2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A module which supports writing ZIP files.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! # Example
 | 
				
			||||||
 | 
					//! ### Whole data (u8 slice)
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					//! # {
 | 
				
			||||||
 | 
					//! # use async_zip::{Compression, ZipEntryBuilder, base::write::ZipFileWriter};
 | 
				
			||||||
 | 
					//! # use async_zip::error::ZipError;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! # async fn run() -> Result<(), ZipError> {
 | 
				
			||||||
 | 
					//! let mut writer = ZipFileWriter::new(Vec::<u8>::new());
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! let data = b"This is an example file.";
 | 
				
			||||||
 | 
					//! let opts = ZipEntryBuilder::new(String::from("foo.txt").into(), Compression::Deflate);
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! writer.write_entry_whole(opts, data).await?;
 | 
				
			||||||
 | 
					//! writer.close().await?;
 | 
				
			||||||
 | 
					//! #   Ok(())
 | 
				
			||||||
 | 
					//! # }
 | 
				
			||||||
 | 
					//! # }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					//! ### Stream data (unknown size & data)
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					//! # {
 | 
				
			||||||
 | 
					//! # use async_zip::{Compression, ZipEntryBuilder, base::write::ZipFileWriter};
 | 
				
			||||||
 | 
					//! # use std::io::Cursor;
 | 
				
			||||||
 | 
					//! # use async_zip::error::ZipError;
 | 
				
			||||||
 | 
					//! # use futures_lite::io::AsyncWriteExt;
 | 
				
			||||||
 | 
					//! # use tokio_util::compat::TokioAsyncWriteCompatExt;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! # async fn run() -> Result<(), ZipError> {
 | 
				
			||||||
 | 
					//! let mut writer = ZipFileWriter::new(Vec::<u8>::new());
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! let data = b"This is an example file.";
 | 
				
			||||||
 | 
					//! let opts = ZipEntryBuilder::new(String::from("bar.txt").into(), Compression::Deflate);
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! let mut entry_writer = writer.write_entry_stream(opts).await?;
 | 
				
			||||||
 | 
					//! entry_writer.write_all(data).await.unwrap();
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! entry_writer.close().await?;
 | 
				
			||||||
 | 
					//! writer.close().await?;
 | 
				
			||||||
 | 
					//! #   Ok(())
 | 
				
			||||||
 | 
					//! # }
 | 
				
			||||||
 | 
					//! # }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod compressed_writer;
 | 
				
			||||||
 | 
					pub(crate) mod entry_stream;
 | 
				
			||||||
 | 
					pub(crate) mod entry_whole;
 | 
				
			||||||
 | 
					pub(crate) mod io;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use entry_stream::EntryStreamWriter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::entry::ZipEntry;
 | 
				
			||||||
 | 
					use crate::error::Result;
 | 
				
			||||||
 | 
					use crate::spec::extra_field::ExtraFieldAsBytes;
 | 
				
			||||||
 | 
					use crate::spec::header::{
 | 
				
			||||||
 | 
					    CentralDirectoryRecord, EndOfCentralDirectoryHeader, ExtraField, InfoZipUnicodeCommentExtraField,
 | 
				
			||||||
 | 
					    InfoZipUnicodePathExtraField, Zip64EndOfCentralDirectoryLocator, Zip64EndOfCentralDirectoryRecord,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					use crate::tokio::write::ZipFileWriter as TokioZipFileWriter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use entry_whole::EntryWholeWriter;
 | 
				
			||||||
 | 
					use io::offset::AsyncOffsetWriter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::spec::consts::{NON_ZIP64_MAX_NUM_FILES, NON_ZIP64_MAX_SIZE};
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncWrite, AsyncWriteExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) struct CentralDirectoryEntry {
 | 
				
			||||||
 | 
					    pub header: CentralDirectoryRecord,
 | 
				
			||||||
 | 
					    pub entry: ZipEntry,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A ZIP file writer which acts over AsyncWrite implementers.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// # Note
 | 
				
			||||||
 | 
					/// - [`ZipFileWriter::close()`] must be called before a stream writer goes out of scope.
 | 
				
			||||||
 | 
					pub struct ZipFileWriter<W> {
 | 
				
			||||||
 | 
					    pub(crate) writer: AsyncOffsetWriter<W>,
 | 
				
			||||||
 | 
					    pub(crate) cd_entries: Vec<CentralDirectoryEntry>,
 | 
				
			||||||
 | 
					    /// If true, will error if a Zip64 struct must be written.
 | 
				
			||||||
 | 
					    force_no_zip64: bool,
 | 
				
			||||||
 | 
					    /// Whether to write Zip64 end of directory structs.
 | 
				
			||||||
 | 
					    pub(crate) is_zip64: bool,
 | 
				
			||||||
 | 
					    comment_opt: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<W: AsyncWrite + Unpin> ZipFileWriter<W> {
 | 
				
			||||||
 | 
					    /// Construct a new ZIP file writer from a mutable reference to a writer.
 | 
				
			||||||
 | 
					    pub fn new(writer: W) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            writer: AsyncOffsetWriter::new(writer),
 | 
				
			||||||
 | 
					            cd_entries: Vec::new(),
 | 
				
			||||||
 | 
					            comment_opt: None,
 | 
				
			||||||
 | 
					            is_zip64: false,
 | 
				
			||||||
 | 
					            force_no_zip64: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Force the ZIP writer to operate in non-ZIP64 mode.
 | 
				
			||||||
 | 
					    /// If any files would need ZIP64, an error will be raised.
 | 
				
			||||||
 | 
					    pub fn force_no_zip64(mut self) -> Self {
 | 
				
			||||||
 | 
					        self.force_no_zip64 = true;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Force the ZIP writer to emit Zip64 structs at the end of the archive.
 | 
				
			||||||
 | 
					    /// Zip64 extended fields will only be written if needed.
 | 
				
			||||||
 | 
					    pub fn force_zip64(mut self) -> Self {
 | 
				
			||||||
 | 
					        self.is_zip64 = true;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Write a new ZIP entry of known size and data.
 | 
				
			||||||
 | 
					    pub async fn write_entry_whole<E: Into<ZipEntry>>(&mut self, entry: E, data: &[u8]) -> Result<()> {
 | 
				
			||||||
 | 
					        EntryWholeWriter::from_raw(self, entry.into(), data).write().await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Write an entry of unknown size and data via streaming (ie. using a data descriptor).
 | 
				
			||||||
 | 
					    /// The generated Local File Header will be invalid, with no compressed size, uncompressed size,
 | 
				
			||||||
 | 
					    /// and a null CRC. This might cause problems with the destination reader.
 | 
				
			||||||
 | 
					    pub async fn write_entry_stream<E: Into<ZipEntry>>(&mut self, entry: E) -> Result<EntryStreamWriter<'_, W>> {
 | 
				
			||||||
 | 
					        EntryStreamWriter::from_raw(self, entry.into()).await
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Set the ZIP file comment.
 | 
				
			||||||
 | 
					    pub fn comment(&mut self, comment: String) {
 | 
				
			||||||
 | 
					        self.comment_opt = Some(comment);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a mutable reference to the inner writer.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Care should be taken when using this inner writer as doing so may invalidate internal state of this writer.
 | 
				
			||||||
 | 
					    pub fn inner_mut(&mut self) -> &mut W {
 | 
				
			||||||
 | 
					        self.writer.inner_mut()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this ZIP writer and completes all closing tasks.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This includes:
 | 
				
			||||||
 | 
					    /// - Writing all central directory headers.
 | 
				
			||||||
 | 
					    /// - Writing the end of central directory header.
 | 
				
			||||||
 | 
					    /// - Writing the file comment.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Failure to call this function before going out of scope would result in a corrupted ZIP file.
 | 
				
			||||||
 | 
					    pub async fn close(mut self) -> Result<W> {
 | 
				
			||||||
 | 
					        let cd_offset = self.writer.offset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for entry in &self.cd_entries {
 | 
				
			||||||
 | 
					            let filename_basic =
 | 
				
			||||||
 | 
					                entry.entry.filename().alternative().unwrap_or_else(|| entry.entry.filename().as_bytes());
 | 
				
			||||||
 | 
					            let comment_basic = entry.entry.comment().alternative().unwrap_or_else(|| entry.entry.comment().as_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.writer.write_all(&crate::spec::consts::CDH_SIGNATURE.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					            self.writer.write_all(&entry.header.as_slice()).await?;
 | 
				
			||||||
 | 
					            self.writer.write_all(filename_basic).await?;
 | 
				
			||||||
 | 
					            self.writer.write_all(&entry.entry.extra_fields().as_bytes()).await?;
 | 
				
			||||||
 | 
					            self.writer.write_all(comment_basic).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let central_directory_size = self.writer.offset() - cd_offset;
 | 
				
			||||||
 | 
					        let central_directory_size_u32 = if central_directory_size > NON_ZIP64_MAX_SIZE as u64 {
 | 
				
			||||||
 | 
					            NON_ZIP64_MAX_SIZE
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            central_directory_size as u32
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let num_entries_in_directory = self.cd_entries.len() as u64;
 | 
				
			||||||
 | 
					        let num_entries_in_directory_u16 = if num_entries_in_directory > NON_ZIP64_MAX_NUM_FILES as u64 {
 | 
				
			||||||
 | 
					            NON_ZIP64_MAX_NUM_FILES
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            num_entries_in_directory as u16
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let cd_offset_u32 = if cd_offset > NON_ZIP64_MAX_SIZE as u64 {
 | 
				
			||||||
 | 
					            if self.force_no_zip64 {
 | 
				
			||||||
 | 
					                return Err(crate::error::ZipError::Zip64Needed(crate::error::Zip64ErrorCase::LargeFile));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                self.is_zip64 = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            NON_ZIP64_MAX_SIZE
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            cd_offset as u32
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add the zip64 EOCDR and EOCDL if we are in zip64 mode.
 | 
				
			||||||
 | 
					        if self.is_zip64 {
 | 
				
			||||||
 | 
					            let eocdr_offset = self.writer.offset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let eocdr = Zip64EndOfCentralDirectoryRecord {
 | 
				
			||||||
 | 
					                size_of_zip64_end_of_cd_record: 44,
 | 
				
			||||||
 | 
					                version_made_by: crate::spec::version::as_made_by(),
 | 
				
			||||||
 | 
					                version_needed_to_extract: 46,
 | 
				
			||||||
 | 
					                disk_number: 0,
 | 
				
			||||||
 | 
					                disk_number_start_of_cd: 0,
 | 
				
			||||||
 | 
					                num_entries_in_directory_on_disk: num_entries_in_directory,
 | 
				
			||||||
 | 
					                num_entries_in_directory,
 | 
				
			||||||
 | 
					                directory_size: central_directory_size,
 | 
				
			||||||
 | 
					                offset_of_start_of_directory: cd_offset,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            self.writer.write_all(&crate::spec::consts::ZIP64_EOCDR_SIGNATURE.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					            self.writer.write_all(&eocdr.as_bytes()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let eocdl = Zip64EndOfCentralDirectoryLocator {
 | 
				
			||||||
 | 
					                number_of_disk_with_start_of_zip64_end_of_central_directory: 0,
 | 
				
			||||||
 | 
					                relative_offset: eocdr_offset,
 | 
				
			||||||
 | 
					                total_number_of_disks: 1,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            self.writer.write_all(&crate::spec::consts::ZIP64_EOCDL_SIGNATURE.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					            self.writer.write_all(&eocdl.as_bytes()).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let header = EndOfCentralDirectoryHeader {
 | 
				
			||||||
 | 
					            disk_num: 0,
 | 
				
			||||||
 | 
					            start_cent_dir_disk: 0,
 | 
				
			||||||
 | 
					            num_of_entries_disk: num_entries_in_directory_u16,
 | 
				
			||||||
 | 
					            num_of_entries: num_entries_in_directory_u16,
 | 
				
			||||||
 | 
					            size_cent_dir: central_directory_size_u32,
 | 
				
			||||||
 | 
					            cent_dir_offset: cd_offset_u32,
 | 
				
			||||||
 | 
					            file_comm_length: self.comment_opt.as_ref().map(|v| v.len() as u16).unwrap_or_default(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.writer.write_all(&crate::spec::consts::EOCDR_SIGNATURE.to_le_bytes()).await?;
 | 
				
			||||||
 | 
					        self.writer.write_all(&header.as_slice()).await?;
 | 
				
			||||||
 | 
					        if let Some(comment) = self.comment_opt {
 | 
				
			||||||
 | 
					            self.writer.write_all(comment.as_bytes()).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(self.writer.into_inner())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					impl<W> ZipFileWriter<Compat<W>>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    W: tokio::io::AsyncWrite + Unpin,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /// Construct a new ZIP file writer from a mutable reference to a writer.
 | 
				
			||||||
 | 
					    pub fn with_tokio(writer: W) -> TokioZipFileWriter<W> {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            writer: AsyncOffsetWriter::new(writer.compat_write()),
 | 
				
			||||||
 | 
					            cd_entries: Vec::new(),
 | 
				
			||||||
 | 
					            comment_opt: None,
 | 
				
			||||||
 | 
					            is_zip64: false,
 | 
				
			||||||
 | 
					            force_no_zip64: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) fn get_or_put_info_zip_unicode_path_extra_field_mut(
 | 
				
			||||||
 | 
					    extra_fields: &mut Vec<ExtraField>,
 | 
				
			||||||
 | 
					) -> &mut InfoZipUnicodePathExtraField {
 | 
				
			||||||
 | 
					    if !extra_fields.iter().any(|field| matches!(field, ExtraField::InfoZipUnicodePath(_))) {
 | 
				
			||||||
 | 
					        extra_fields
 | 
				
			||||||
 | 
					            .push(ExtraField::InfoZipUnicodePath(InfoZipUnicodePathExtraField::V1 { crc32: 0, unicode: vec![] }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for field in extra_fields.iter_mut() {
 | 
				
			||||||
 | 
					        if let ExtraField::InfoZipUnicodePath(extra_field) = field {
 | 
				
			||||||
 | 
					            return extra_field;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    panic!("InfoZipUnicodePathExtraField not found after insertion")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) fn get_or_put_info_zip_unicode_comment_extra_field_mut(
 | 
				
			||||||
 | 
					    extra_fields: &mut Vec<ExtraField>,
 | 
				
			||||||
 | 
					) -> &mut InfoZipUnicodeCommentExtraField {
 | 
				
			||||||
 | 
					    if !extra_fields.iter().any(|field| matches!(field, ExtraField::InfoZipUnicodeComment(_))) {
 | 
				
			||||||
 | 
					        extra_fields
 | 
				
			||||||
 | 
					            .push(ExtraField::InfoZipUnicodeComment(InfoZipUnicodeCommentExtraField::V1 { crc32: 0, unicode: vec![] }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for field in extra_fields.iter_mut() {
 | 
				
			||||||
 | 
					        if let ExtraField::InfoZipUnicodeComment(extra_field) = field {
 | 
				
			||||||
 | 
					            return extra_field;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    panic!("InfoZipUnicodeCommentExtraField not found after insertion")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										83
									
								
								crates/async_zip/src/date/builder.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								crates/async_zip/src/date/builder.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,83 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2024 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::ZipDateTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A builder for [`ZipDateTime`].
 | 
				
			||||||
 | 
					pub struct ZipDateTimeBuilder(pub(crate) ZipDateTime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<ZipDateTime> for ZipDateTimeBuilder {
 | 
				
			||||||
 | 
					    fn from(date: ZipDateTime) -> Self {
 | 
				
			||||||
 | 
					        Self(date)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for ZipDateTimeBuilder {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self::new()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipDateTimeBuilder {
 | 
				
			||||||
 | 
					    /// Constructs a new builder which defines the raw underlying data of a ZIP entry.
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        Self(ZipDateTime { date: 0, time: 0 })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the date and time's year.
 | 
				
			||||||
 | 
					    pub fn year(mut self, year: i32) -> Self {
 | 
				
			||||||
 | 
					        let year: u16 = (((year - 1980) << 9) & 0xFE00).try_into().unwrap();
 | 
				
			||||||
 | 
					        self.0.date |= year;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the date and time's month.
 | 
				
			||||||
 | 
					    pub fn month(mut self, month: u32) -> Self {
 | 
				
			||||||
 | 
					        let month: u16 = ((month << 5) & 0x1E0).try_into().unwrap();
 | 
				
			||||||
 | 
					        self.0.date |= month;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the date and time's day.
 | 
				
			||||||
 | 
					    pub fn day(mut self, day: u32) -> Self {
 | 
				
			||||||
 | 
					        let day: u16 = (day & 0x1F).try_into().unwrap();
 | 
				
			||||||
 | 
					        self.0.date |= day;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the date and time's hour.
 | 
				
			||||||
 | 
					    pub fn hour(mut self, hour: u32) -> Self {
 | 
				
			||||||
 | 
					        let hour: u16 = ((hour << 11) & 0xF800).try_into().unwrap();
 | 
				
			||||||
 | 
					        self.0.time |= hour;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the date and time's minute.
 | 
				
			||||||
 | 
					    pub fn minute(mut self, minute: u32) -> Self {
 | 
				
			||||||
 | 
					        let minute: u16 = ((minute << 5) & 0x7E0).try_into().unwrap();
 | 
				
			||||||
 | 
					        self.0.time |= minute;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the date and time's second.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Note that MS-DOS has a maximum granularity of two seconds.
 | 
				
			||||||
 | 
					    pub fn second(mut self, second: u32) -> Self {
 | 
				
			||||||
 | 
					        let second: u16 = ((second >> 1) & 0x1F).try_into().unwrap();
 | 
				
			||||||
 | 
					        self.0.time |= second;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this builder and returns a final [`ZipDateTime`].
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This is equivalent to:
 | 
				
			||||||
 | 
					    /// ```
 | 
				
			||||||
 | 
					    /// # use async_zip::{ZipDateTime, ZipDateTimeBuilder, Compression};
 | 
				
			||||||
 | 
					    /// #
 | 
				
			||||||
 | 
					    /// # let builder = ZipDateTimeBuilder::new().year(2024).month(3).day(2);
 | 
				
			||||||
 | 
					    /// let date: ZipDateTime = builder.into();
 | 
				
			||||||
 | 
					    /// ```
 | 
				
			||||||
 | 
					    pub fn build(self) -> ZipDateTime {
 | 
				
			||||||
 | 
					        self.into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										112
									
								
								crates/async_zip/src/date/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								crates/async_zip/src/date/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,112 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021-2024 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod builder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					use chrono::{DateTime, Datelike, LocalResult, TimeZone, Timelike, Utc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use self::builder::ZipDateTimeBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#446
 | 
				
			||||||
 | 
					// https://learn.microsoft.com/en-us/windows/win32/api/oleauto/nf-oleauto-dosdatetimetovarianttime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A date and time stored as per the MS-DOS representation used by ZIP files.
 | 
				
			||||||
 | 
					#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)]
 | 
				
			||||||
 | 
					pub struct ZipDateTime {
 | 
				
			||||||
 | 
					    pub(crate) date: u16,
 | 
				
			||||||
 | 
					    pub(crate) time: u16,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipDateTime {
 | 
				
			||||||
 | 
					    /// Returns the year of this date & time.
 | 
				
			||||||
 | 
					    pub fn year(&self) -> i32 {
 | 
				
			||||||
 | 
					        (((self.date & 0xFE00) >> 9) + 1980).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the month of this date & time.
 | 
				
			||||||
 | 
					    pub fn month(&self) -> u32 {
 | 
				
			||||||
 | 
					        ((self.date & 0x1E0) >> 5).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the day of this date & time.
 | 
				
			||||||
 | 
					    pub fn day(&self) -> u32 {
 | 
				
			||||||
 | 
					        (self.date & 0x1F).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the hour of this date & time.
 | 
				
			||||||
 | 
					    pub fn hour(&self) -> u32 {
 | 
				
			||||||
 | 
					        ((self.time & 0xF800) >> 11).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the minute of this date & time.
 | 
				
			||||||
 | 
					    pub fn minute(&self) -> u32 {
 | 
				
			||||||
 | 
					        ((self.time & 0x7E0) >> 5).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the second of this date & time.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Note that MS-DOS has a maximum granularity of two seconds.
 | 
				
			||||||
 | 
					    pub fn second(&self) -> u32 {
 | 
				
			||||||
 | 
					        ((self.time & 0x1F) << 1).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Constructs chrono's [`DateTime`] representation of this date & time.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Note that this requires the `chrono` feature.
 | 
				
			||||||
 | 
					    #[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					    pub fn as_chrono(&self) -> LocalResult<DateTime<Utc>> {
 | 
				
			||||||
 | 
					        self.into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Constructs this date & time from chrono's [`DateTime`] representation.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Note that this requires the `chrono` feature.
 | 
				
			||||||
 | 
					    #[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					    pub fn from_chrono(dt: &DateTime<Utc>) -> Self {
 | 
				
			||||||
 | 
					        dt.into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<ZipDateTimeBuilder> for ZipDateTime {
 | 
				
			||||||
 | 
					    fn from(builder: ZipDateTimeBuilder) -> Self {
 | 
				
			||||||
 | 
					        builder.0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					impl From<&DateTime<Utc>> for ZipDateTime {
 | 
				
			||||||
 | 
					    fn from(value: &DateTime<Utc>) -> Self {
 | 
				
			||||||
 | 
					        let mut builder = ZipDateTimeBuilder::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        builder = builder.year(value.date_naive().year());
 | 
				
			||||||
 | 
					        builder = builder.month(value.date_naive().month());
 | 
				
			||||||
 | 
					        builder = builder.day(value.date_naive().day());
 | 
				
			||||||
 | 
					        builder = builder.hour(value.time().hour());
 | 
				
			||||||
 | 
					        builder = builder.minute(value.time().minute());
 | 
				
			||||||
 | 
					        builder = builder.second(value.time().second());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        builder.build()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					impl From<&ZipDateTime> for LocalResult<DateTime<Utc>> {
 | 
				
			||||||
 | 
					    fn from(value: &ZipDateTime) -> Self {
 | 
				
			||||||
 | 
					        Utc.with_ymd_and_hms(value.year(), value.month(), value.day(), value.hour(), value.minute(), value.second())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					impl From<DateTime<Utc>> for ZipDateTime {
 | 
				
			||||||
 | 
					    fn from(value: DateTime<Utc>) -> Self {
 | 
				
			||||||
 | 
					        (&value).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					impl From<ZipDateTime> for LocalResult<DateTime<Utc>> {
 | 
				
			||||||
 | 
					    fn from(value: ZipDateTime) -> Self {
 | 
				
			||||||
 | 
					        (&value).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										113
									
								
								crates/async_zip/src/entry/builder.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								crates/async_zip/src/entry/builder.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::entry::ZipEntry;
 | 
				
			||||||
 | 
					use crate::spec::{attribute::AttributeCompatibility, header::ExtraField, Compression};
 | 
				
			||||||
 | 
					use crate::{date::ZipDateTime, string::ZipString};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A builder for [`ZipEntry`].
 | 
				
			||||||
 | 
					pub struct ZipEntryBuilder(pub(crate) ZipEntry);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<ZipEntry> for ZipEntryBuilder {
 | 
				
			||||||
 | 
					    fn from(entry: ZipEntry) -> Self {
 | 
				
			||||||
 | 
					        Self(entry)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipEntryBuilder {
 | 
				
			||||||
 | 
					    /// Constructs a new builder which defines the raw underlying data of a ZIP entry.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// A filename and compression method are needed to construct the builder as minimal parameters.
 | 
				
			||||||
 | 
					    pub fn new(filename: ZipString, compression: Compression) -> Self {
 | 
				
			||||||
 | 
					        Self(ZipEntry::new(filename, compression))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's filename.
 | 
				
			||||||
 | 
					    pub fn filename(mut self, filename: ZipString) -> Self {
 | 
				
			||||||
 | 
					        self.0.filename = filename;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's compression method.
 | 
				
			||||||
 | 
					    pub fn compression(mut self, compression: Compression) -> Self {
 | 
				
			||||||
 | 
					        self.0.compression = compression;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Set a size hint for the file, to be written into the local file header.
 | 
				
			||||||
 | 
					    /// Unlikely to be useful except for the case of streaming files to be Store'd.
 | 
				
			||||||
 | 
					    /// This size hint does not affect the central directory, nor does it affect whole files.
 | 
				
			||||||
 | 
					    pub fn size<N: Into<u64>, M: Into<u64>>(mut self, compressed_size: N, uncompressed_size: M) -> Self {
 | 
				
			||||||
 | 
					        self.0.compressed_size = compressed_size.into();
 | 
				
			||||||
 | 
					        self.0.uncompressed_size = uncompressed_size.into();
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Set the deflate compression option.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// If the compression type isn't deflate, this option has no effect.
 | 
				
			||||||
 | 
					    #[cfg(any(feature = "deflate", feature = "bzip2", feature = "zstd", feature = "lzma", feature = "xz"))]
 | 
				
			||||||
 | 
					    pub fn deflate_option(mut self, option: crate::DeflateOption) -> Self {
 | 
				
			||||||
 | 
					        self.0.compression_level = option.into_level();
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's attribute host compatibility.
 | 
				
			||||||
 | 
					    pub fn attribute_compatibility(mut self, compatibility: AttributeCompatibility) -> Self {
 | 
				
			||||||
 | 
					        self.0.attribute_compatibility = compatibility;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's last modification date.
 | 
				
			||||||
 | 
					    pub fn last_modification_date(mut self, date: ZipDateTime) -> Self {
 | 
				
			||||||
 | 
					        self.0.last_modification_date = date;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's internal file attribute.
 | 
				
			||||||
 | 
					    pub fn internal_file_attribute(mut self, attribute: u16) -> Self {
 | 
				
			||||||
 | 
					        self.0.internal_file_attribute = attribute;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's external file attribute.
 | 
				
			||||||
 | 
					    pub fn external_file_attribute(mut self, attribute: u32) -> Self {
 | 
				
			||||||
 | 
					        self.0.external_file_attribute = attribute;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's extra field data.
 | 
				
			||||||
 | 
					    pub fn extra_fields(mut self, field: Vec<ExtraField>) -> Self {
 | 
				
			||||||
 | 
					        self.0.extra_fields = field;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's file comment.
 | 
				
			||||||
 | 
					    pub fn comment(mut self, comment: ZipString) -> Self {
 | 
				
			||||||
 | 
					        self.0.comment = comment;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the entry's Unix permissions mode.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// If the attribute host compatibility isn't set to Unix, this will have no effect.
 | 
				
			||||||
 | 
					    pub fn unix_permissions(mut self, mode: u16) -> Self {
 | 
				
			||||||
 | 
					        if matches!(self.0.attribute_compatibility, AttributeCompatibility::Unix) {
 | 
				
			||||||
 | 
					            self.0.external_file_attribute = (self.0.external_file_attribute & 0xFFFF) | (mode as u32) << 16;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this builder and returns a final [`ZipEntry`].
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This is equivalent to:
 | 
				
			||||||
 | 
					    /// ```
 | 
				
			||||||
 | 
					    /// # use async_zip::{ZipEntry, ZipEntryBuilder, Compression};
 | 
				
			||||||
 | 
					    /// #
 | 
				
			||||||
 | 
					    /// # let builder = ZipEntryBuilder::new(String::from("foo.bar").into(), Compression::Stored);
 | 
				
			||||||
 | 
					    /// let entry: ZipEntry = builder.into();
 | 
				
			||||||
 | 
					    /// ```
 | 
				
			||||||
 | 
					    pub fn build(self) -> ZipEntry {
 | 
				
			||||||
 | 
					        self.into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										219
									
								
								crates/async_zip/src/entry/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								crates/async_zip/src/entry/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,219 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod builder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::ops::Deref;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, SeekFrom};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::entry::builder::ZipEntryBuilder;
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::{
 | 
				
			||||||
 | 
					    attribute::AttributeCompatibility,
 | 
				
			||||||
 | 
					    consts::LFH_SIGNATURE,
 | 
				
			||||||
 | 
					    header::{ExtraField, LocalFileHeader},
 | 
				
			||||||
 | 
					    Compression,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use crate::{string::ZipString, ZipDateTime};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An immutable store of data about a ZIP entry.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This type cannot be directly constructed so instead, the [`ZipEntryBuilder`] must be used. Internally this builder
 | 
				
			||||||
 | 
					/// stores a [`ZipEntry`] so conversions between these two types via the [`From`] implementations will be
 | 
				
			||||||
 | 
					/// non-allocating.
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct ZipEntry {
 | 
				
			||||||
 | 
					    pub(crate) filename: ZipString,
 | 
				
			||||||
 | 
					    pub(crate) compression: Compression,
 | 
				
			||||||
 | 
					    #[cfg(any(
 | 
				
			||||||
 | 
					        feature = "deflate",
 | 
				
			||||||
 | 
					        feature = "bzip2",
 | 
				
			||||||
 | 
					        feature = "zstd",
 | 
				
			||||||
 | 
					        feature = "lzma",
 | 
				
			||||||
 | 
					        feature = "xz",
 | 
				
			||||||
 | 
					        feature = "deflate64"
 | 
				
			||||||
 | 
					    ))]
 | 
				
			||||||
 | 
					    pub(crate) compression_level: async_compression::Level,
 | 
				
			||||||
 | 
					    pub(crate) crc32: u32,
 | 
				
			||||||
 | 
					    pub(crate) uncompressed_size: u64,
 | 
				
			||||||
 | 
					    pub(crate) compressed_size: u64,
 | 
				
			||||||
 | 
					    pub(crate) attribute_compatibility: AttributeCompatibility,
 | 
				
			||||||
 | 
					    pub(crate) last_modification_date: ZipDateTime,
 | 
				
			||||||
 | 
					    pub(crate) internal_file_attribute: u16,
 | 
				
			||||||
 | 
					    pub(crate) external_file_attribute: u32,
 | 
				
			||||||
 | 
					    pub(crate) extra_fields: Vec<ExtraField>,
 | 
				
			||||||
 | 
					    pub(crate) comment: ZipString,
 | 
				
			||||||
 | 
					    pub(crate) data_descriptor: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<ZipEntryBuilder> for ZipEntry {
 | 
				
			||||||
 | 
					    fn from(builder: ZipEntryBuilder) -> Self {
 | 
				
			||||||
 | 
					        builder.0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipEntry {
 | 
				
			||||||
 | 
					    pub(crate) fn new(filename: ZipString, compression: Compression) -> Self {
 | 
				
			||||||
 | 
					        ZipEntry {
 | 
				
			||||||
 | 
					            filename,
 | 
				
			||||||
 | 
					            compression,
 | 
				
			||||||
 | 
					            #[cfg(any(
 | 
				
			||||||
 | 
					                feature = "deflate",
 | 
				
			||||||
 | 
					                feature = "bzip2",
 | 
				
			||||||
 | 
					                feature = "zstd",
 | 
				
			||||||
 | 
					                feature = "lzma",
 | 
				
			||||||
 | 
					                feature = "xz",
 | 
				
			||||||
 | 
					                feature = "deflate64"
 | 
				
			||||||
 | 
					            ))]
 | 
				
			||||||
 | 
					            compression_level: async_compression::Level::Default,
 | 
				
			||||||
 | 
					            crc32: 0,
 | 
				
			||||||
 | 
					            uncompressed_size: 0,
 | 
				
			||||||
 | 
					            compressed_size: 0,
 | 
				
			||||||
 | 
					            attribute_compatibility: AttributeCompatibility::Unix,
 | 
				
			||||||
 | 
					            last_modification_date: ZipDateTime::default(),
 | 
				
			||||||
 | 
					            internal_file_attribute: 0,
 | 
				
			||||||
 | 
					            external_file_attribute: 0,
 | 
				
			||||||
 | 
					            extra_fields: Vec::new(),
 | 
				
			||||||
 | 
					            comment: String::new().into(),
 | 
				
			||||||
 | 
					            data_descriptor: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's filename.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// ## Note
 | 
				
			||||||
 | 
					    /// This will return the raw filename stored during ZIP creation. If calling this method on entries retrieved from
 | 
				
			||||||
 | 
					    /// untrusted ZIP files, the filename should be sanitised before being used as a path to prevent [directory
 | 
				
			||||||
 | 
					    /// traversal attacks](https://en.wikipedia.org/wiki/Directory_traversal_attack).
 | 
				
			||||||
 | 
					    pub fn filename(&self) -> &ZipString {
 | 
				
			||||||
 | 
					        &self.filename
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's compression method.
 | 
				
			||||||
 | 
					    pub fn compression(&self) -> Compression {
 | 
				
			||||||
 | 
					        self.compression
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's CRC32 value.
 | 
				
			||||||
 | 
					    pub fn crc32(&self) -> u32 {
 | 
				
			||||||
 | 
					        self.crc32
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's uncompressed size.
 | 
				
			||||||
 | 
					    pub fn uncompressed_size(&self) -> u64 {
 | 
				
			||||||
 | 
					        self.uncompressed_size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's compressed size.
 | 
				
			||||||
 | 
					    pub fn compressed_size(&self) -> u64 {
 | 
				
			||||||
 | 
					        self.compressed_size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's attribute's host compatibility.
 | 
				
			||||||
 | 
					    pub fn attribute_compatibility(&self) -> AttributeCompatibility {
 | 
				
			||||||
 | 
					        self.attribute_compatibility
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's last modification time & date.
 | 
				
			||||||
 | 
					    pub fn last_modification_date(&self) -> &ZipDateTime {
 | 
				
			||||||
 | 
					        &self.last_modification_date
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's internal file attribute.
 | 
				
			||||||
 | 
					    pub fn internal_file_attribute(&self) -> u16 {
 | 
				
			||||||
 | 
					        self.internal_file_attribute
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's external file attribute
 | 
				
			||||||
 | 
					    pub fn external_file_attribute(&self) -> u32 {
 | 
				
			||||||
 | 
					        self.external_file_attribute
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's extra field data.
 | 
				
			||||||
 | 
					    pub fn extra_fields(&self) -> &[ExtraField] {
 | 
				
			||||||
 | 
					        &self.extra_fields
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's file comment.
 | 
				
			||||||
 | 
					    pub fn comment(&self) -> &ZipString {
 | 
				
			||||||
 | 
					        &self.comment
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the entry's integer-based UNIX permissions.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// # Note
 | 
				
			||||||
 | 
					    /// This will return None if the attribute host compatibility is not listed as Unix.
 | 
				
			||||||
 | 
					    pub fn unix_permissions(&self) -> Option<u16> {
 | 
				
			||||||
 | 
					        if !matches!(self.attribute_compatibility, AttributeCompatibility::Unix) {
 | 
				
			||||||
 | 
					            return None;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Some(((self.external_file_attribute) >> 16) as u16)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns whether or not the entry represents a directory.
 | 
				
			||||||
 | 
					    pub fn dir(&self) -> Result<bool> {
 | 
				
			||||||
 | 
					        Ok(self.filename.as_str()?.ends_with('/'))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An immutable store of data about how a ZIP entry is stored within a specific archive.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Besides storing archive independent information like the size and timestamp it can also be used to query
 | 
				
			||||||
 | 
					/// information about how the entry is stored in an archive.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct StoredZipEntry {
 | 
				
			||||||
 | 
					    pub(crate) entry: ZipEntry,
 | 
				
			||||||
 | 
					    // pub(crate) general_purpose_flag: GeneralPurposeFlag,
 | 
				
			||||||
 | 
					    pub(crate) file_offset: u64,
 | 
				
			||||||
 | 
					    pub(crate) header_size: u64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl StoredZipEntry {
 | 
				
			||||||
 | 
					    /// Returns the offset in bytes to where the header of the entry starts.
 | 
				
			||||||
 | 
					    pub fn header_offset(&self) -> u64 {
 | 
				
			||||||
 | 
					        self.file_offset
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the combined size in bytes of the header, the filename, and any extra fields.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Note: This uses the extra field length stored in the central directory, which may differ from that stored in
 | 
				
			||||||
 | 
					    /// the local file header. See specification: <https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#732>
 | 
				
			||||||
 | 
					    pub fn header_size(&self) -> u64 {
 | 
				
			||||||
 | 
					        self.header_size
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Seek to the offset in bytes where the data of the entry starts.
 | 
				
			||||||
 | 
					    pub(crate) async fn seek_to_data_offset<R: AsyncRead + AsyncSeek + Unpin>(&self, mut reader: &mut R) -> Result<()> {
 | 
				
			||||||
 | 
					        // Seek to the header
 | 
				
			||||||
 | 
					        reader.seek(SeekFrom::Start(self.file_offset)).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check the signature
 | 
				
			||||||
 | 
					        let signature = {
 | 
				
			||||||
 | 
					            let mut buffer = [0; 4];
 | 
				
			||||||
 | 
					            reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					            u32::from_le_bytes(buffer)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match signature {
 | 
				
			||||||
 | 
					            LFH_SIGNATURE => (),
 | 
				
			||||||
 | 
					            actual => return Err(ZipError::UnexpectedHeaderError(actual, LFH_SIGNATURE)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Skip the local file header and trailing data
 | 
				
			||||||
 | 
					        let header = LocalFileHeader::from_reader(&mut reader).await?;
 | 
				
			||||||
 | 
					        let trailing_size = (header.file_name_length as i64) + (header.extra_field_length as i64);
 | 
				
			||||||
 | 
					        reader.seek(SeekFrom::Current(trailing_size)).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Deref for StoredZipEntry {
 | 
				
			||||||
 | 
					    type Target = ZipEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn deref(&self) -> &Self::Target {
 | 
				
			||||||
 | 
					        &self.entry
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										72
									
								
								crates/async_zip/src/error.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								crates/async_zip/src/error.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A module which holds relevant error reporting structures/types.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::fmt::{Display, Formatter};
 | 
				
			||||||
 | 
					use thiserror::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A Result type alias over ZipError to minimise repetition.
 | 
				
			||||||
 | 
					pub type Result<V> = std::result::Result<V, ZipError>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub enum Zip64ErrorCase {
 | 
				
			||||||
 | 
					    TooManyFiles,
 | 
				
			||||||
 | 
					    LargeFile,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Display for Zip64ErrorCase {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::TooManyFiles => write!(f, "More than 65536 files in archive"),
 | 
				
			||||||
 | 
					            Self::LargeFile => write!(f, "File is larger than 4 GiB"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An enum of possible errors and their descriptions.
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug, Error)]
 | 
				
			||||||
 | 
					pub enum ZipError {
 | 
				
			||||||
 | 
					    #[error("feature not supported: '{0}'")]
 | 
				
			||||||
 | 
					    FeatureNotSupported(&'static str),
 | 
				
			||||||
 | 
					    #[error("compression not supported: {0}")]
 | 
				
			||||||
 | 
					    CompressionNotSupported(u16),
 | 
				
			||||||
 | 
					    #[error("host attribute compatibility not supported: {0}")]
 | 
				
			||||||
 | 
					    AttributeCompatibilityNotSupported(u16),
 | 
				
			||||||
 | 
					    #[error("attempted to read a ZIP64 file whilst on a 32-bit target")]
 | 
				
			||||||
 | 
					    TargetZip64NotSupported,
 | 
				
			||||||
 | 
					    #[error("attempted to write a ZIP file with force_no_zip64 when ZIP64 is needed: {0}")]
 | 
				
			||||||
 | 
					    Zip64Needed(Zip64ErrorCase),
 | 
				
			||||||
 | 
					    #[error("end of file has not been reached")]
 | 
				
			||||||
 | 
					    EOFNotReached,
 | 
				
			||||||
 | 
					    #[error("extra fields exceeded maximum size")]
 | 
				
			||||||
 | 
					    ExtraFieldTooLarge,
 | 
				
			||||||
 | 
					    #[error("comment exceeded maximum size")]
 | 
				
			||||||
 | 
					    CommentTooLarge,
 | 
				
			||||||
 | 
					    #[error("filename exceeded maximum size")]
 | 
				
			||||||
 | 
					    FileNameTooLarge,
 | 
				
			||||||
 | 
					    #[error("attempted to convert non-UTF8 bytes to a string/str")]
 | 
				
			||||||
 | 
					    StringNotUtf8,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[error("unable to locate the end of central directory record")]
 | 
				
			||||||
 | 
					    UnableToLocateEOCDR,
 | 
				
			||||||
 | 
					    #[error("extra field size was indicated to be {0} but only {1} bytes remain")]
 | 
				
			||||||
 | 
					    InvalidExtraFieldHeader(u16, usize),
 | 
				
			||||||
 | 
					    #[error("zip64 extended information field was incomplete")]
 | 
				
			||||||
 | 
					    Zip64ExtendedFieldIncomplete,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[error("an upstream reader returned an error: {0}")]
 | 
				
			||||||
 | 
					    UpstreamReadError(#[from] std::io::Error),
 | 
				
			||||||
 | 
					    #[error("a computed CRC32 value did not match the expected value")]
 | 
				
			||||||
 | 
					    CRC32CheckError,
 | 
				
			||||||
 | 
					    #[error("entry index was out of bounds")]
 | 
				
			||||||
 | 
					    EntryIndexOutOfBounds,
 | 
				
			||||||
 | 
					    #[error("Encountered an unexpected header (actual: {0:#x}, expected: {1:#x}).")]
 | 
				
			||||||
 | 
					    UnexpectedHeaderError(u32, u32),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[error("Info-ZIP Unicode Comment Extra Field was incomplete")]
 | 
				
			||||||
 | 
					    InfoZipUnicodeCommentFieldIncomplete,
 | 
				
			||||||
 | 
					    #[error("Info-ZIP Unicode Path Extra Field was incomplete")]
 | 
				
			||||||
 | 
					    InfoZipUnicodePathFieldIncomplete,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										44
									
								
								crates/async_zip/src/file/builder.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								crates/async_zip/src/file/builder.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{file::ZipFile, string::ZipString};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A builder for [`ZipFile`].
 | 
				
			||||||
 | 
					pub struct ZipFileBuilder(pub(crate) ZipFile);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<ZipFile> for ZipFileBuilder {
 | 
				
			||||||
 | 
					    fn from(file: ZipFile) -> Self {
 | 
				
			||||||
 | 
					        Self(file)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for ZipFileBuilder {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        ZipFileBuilder(ZipFile { entries: Vec::new(), zip64: false, comment: String::new().into() })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipFileBuilder {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        Self::default()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Sets the file's comment.
 | 
				
			||||||
 | 
					    pub fn comment(mut self, comment: ZipString) -> Self {
 | 
				
			||||||
 | 
					        self.0.comment = comment;
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Consumes this builder and returns a final [`ZipFile`].
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// This is equivalent to:
 | 
				
			||||||
 | 
					    /// ```
 | 
				
			||||||
 | 
					    /// # use async_zip::{ZipFile, ZipFileBuilder};
 | 
				
			||||||
 | 
					    /// #
 | 
				
			||||||
 | 
					    /// # let builder = ZipFileBuilder::new();
 | 
				
			||||||
 | 
					    /// let file: ZipFile = builder.into();
 | 
				
			||||||
 | 
					    /// ```
 | 
				
			||||||
 | 
					    pub fn build(self) -> ZipFile {
 | 
				
			||||||
 | 
					        self.into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								crates/async_zip/src/file/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								crates/async_zip/src/file/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod builder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{entry::StoredZipEntry, string::ZipString};
 | 
				
			||||||
 | 
					use builder::ZipFileBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An immutable store of data about a ZIP file.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct ZipFile {
 | 
				
			||||||
 | 
					    pub(crate) entries: Vec<StoredZipEntry>,
 | 
				
			||||||
 | 
					    pub(crate) zip64: bool,
 | 
				
			||||||
 | 
					    pub(crate) comment: ZipString,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<ZipFileBuilder> for ZipFile {
 | 
				
			||||||
 | 
					    fn from(builder: ZipFileBuilder) -> Self {
 | 
				
			||||||
 | 
					        builder.0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipFile {
 | 
				
			||||||
 | 
					    /// Returns a list of this ZIP file's entries.
 | 
				
			||||||
 | 
					    pub fn entries(&self) -> &[StoredZipEntry] {
 | 
				
			||||||
 | 
					        &self.entries
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns this ZIP file's trailing comment.
 | 
				
			||||||
 | 
					    pub fn comment(&self) -> &ZipString {
 | 
				
			||||||
 | 
					        &self.comment
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns whether or not this ZIP file is zip64
 | 
				
			||||||
 | 
					    pub fn zip64(&self) -> bool {
 | 
				
			||||||
 | 
					        self.zip64
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								crates/async_zip/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								crates/async_zip/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,62 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021-2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Document all features on docs.rs
 | 
				
			||||||
 | 
					#![cfg_attr(docsrs, feature(doc_cfg))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! An asynchronous ZIP archive reading/writing crate.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ## Features
 | 
				
			||||||
 | 
					//! - A base implementation atop `futures`'s IO traits.
 | 
				
			||||||
 | 
					//! - An extended implementation atop `tokio`'s IO traits.
 | 
				
			||||||
 | 
					//! - Support for Stored, Deflate, bzip2, LZMA, zstd, and xz compression methods.
 | 
				
			||||||
 | 
					//! - Various different reading approaches (seek, stream, filesystem, in-memory buffer).
 | 
				
			||||||
 | 
					//! - Support for writing complete data (u8 slices) or stream writing using data descriptors.
 | 
				
			||||||
 | 
					//! - Initial support for ZIP64 reading and writing.
 | 
				
			||||||
 | 
					//! - Aims for reasonable [specification](https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md) compliance.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ## Installation
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ```toml
 | 
				
			||||||
 | 
					//! [dependencies]
 | 
				
			||||||
 | 
					//! async_zip = { version = "0.0.17", features = ["full"] }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Feature Flags
 | 
				
			||||||
 | 
					//! - `full` - Enables all below features.
 | 
				
			||||||
 | 
					//! - `full-wasm` - Enables all below features that are compatible with WASM.
 | 
				
			||||||
 | 
					//! - `chrono` - Enables support for parsing dates via `chrono`.
 | 
				
			||||||
 | 
					//! - `tokio` - Enables support for the `tokio` implementation module.
 | 
				
			||||||
 | 
					//! - `tokio-fs` - Enables support for the `tokio::fs` reading module.
 | 
				
			||||||
 | 
					//! - `deflate` - Enables support for the Deflate compression method.
 | 
				
			||||||
 | 
					//! - `bzip2` - Enables support for the bzip2 compression method.
 | 
				
			||||||
 | 
					//! - `lzma` - Enables support for the LZMA compression method.
 | 
				
			||||||
 | 
					//! - `zstd` - Enables support for the zstd compression method.
 | 
				
			||||||
 | 
					//! - `xz` - Enables support for the xz compression method.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! [Read more.](https://github.com/Majored/rs-async-zip)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod base;
 | 
				
			||||||
 | 
					pub mod error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					pub mod tokio;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod date;
 | 
				
			||||||
 | 
					pub(crate) mod entry;
 | 
				
			||||||
 | 
					pub(crate) mod file;
 | 
				
			||||||
 | 
					pub(crate) mod spec;
 | 
				
			||||||
 | 
					pub(crate) mod string;
 | 
				
			||||||
 | 
					pub(crate) mod utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					pub(crate) mod tests;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use crate::spec::attribute::AttributeCompatibility;
 | 
				
			||||||
 | 
					pub use crate::spec::compression::{Compression, DeflateOption};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use crate::date::{builder::ZipDateTimeBuilder, ZipDateTime};
 | 
				
			||||||
 | 
					pub use crate::entry::{builder::ZipEntryBuilder, StoredZipEntry, ZipEntry};
 | 
				
			||||||
 | 
					pub use crate::file::{builder::ZipFileBuilder, ZipFile};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use crate::string::{StringEncoding, ZipString};
 | 
				
			||||||
							
								
								
									
										41
									
								
								crates/async_zip/src/spec/attribute.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								crates/async_zip/src/spec/attribute.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An attribute host compatibility supported by this crate.
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub enum AttributeCompatibility {
 | 
				
			||||||
 | 
					    Unix,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TryFrom<u16> for AttributeCompatibility {
 | 
				
			||||||
 | 
					    type Error = ZipError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Convert a u16 stored with little endianness into a supported attribute host compatibility.
 | 
				
			||||||
 | 
					    // https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4422
 | 
				
			||||||
 | 
					    fn try_from(value: u16) -> Result<Self> {
 | 
				
			||||||
 | 
					        match value {
 | 
				
			||||||
 | 
					            3 => Ok(AttributeCompatibility::Unix),
 | 
				
			||||||
 | 
					            _ => Err(ZipError::AttributeCompatibilityNotSupported(value)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<&AttributeCompatibility> for u16 {
 | 
				
			||||||
 | 
					    // Convert a supported attribute host compatibility into its relevant u16 stored with little endianness.
 | 
				
			||||||
 | 
					    // https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4422
 | 
				
			||||||
 | 
					    fn from(compatibility: &AttributeCompatibility) -> Self {
 | 
				
			||||||
 | 
					        match compatibility {
 | 
				
			||||||
 | 
					            AttributeCompatibility::Unix => 3,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<AttributeCompatibility> for u16 {
 | 
				
			||||||
 | 
					    // Convert a supported attribute host compatibility into its relevant u16 stored with little endianness.
 | 
				
			||||||
 | 
					    fn from(compatibility: AttributeCompatibility) -> Self {
 | 
				
			||||||
 | 
					        (&compatibility).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										111
									
								
								crates/async_zip/src/spec/compression.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								crates/async_zip/src/spec/compression.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,111 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(any(feature = "deflate", feature = "bzip2", feature = "zstd", feature = "lzma", feature = "xz"))]
 | 
				
			||||||
 | 
					use async_compression::Level;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A compression method supported by this crate.
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub enum Compression {
 | 
				
			||||||
 | 
					    Stored,
 | 
				
			||||||
 | 
					    #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					    Deflate,
 | 
				
			||||||
 | 
					    #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					    Deflate64,
 | 
				
			||||||
 | 
					    #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					    Bz,
 | 
				
			||||||
 | 
					    #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					    Lzma,
 | 
				
			||||||
 | 
					    #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					    Zstd,
 | 
				
			||||||
 | 
					    #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					    Xz,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TryFrom<u16> for Compression {
 | 
				
			||||||
 | 
					    type Error = ZipError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Convert a u16 stored with little endianness into a supported compression method.
 | 
				
			||||||
 | 
					    // https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#445
 | 
				
			||||||
 | 
					    fn try_from(value: u16) -> Result<Self> {
 | 
				
			||||||
 | 
					        match value {
 | 
				
			||||||
 | 
					            0 => Ok(Compression::Stored),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            8 => Ok(Compression::Deflate),
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					            9 => Ok(Compression::Deflate64),
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            12 => Ok(Compression::Bz),
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            14 => Ok(Compression::Lzma),
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            93 => Ok(Compression::Zstd),
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            95 => Ok(Compression::Xz),
 | 
				
			||||||
 | 
					            _ => Err(ZipError::CompressionNotSupported(value)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<&Compression> for u16 {
 | 
				
			||||||
 | 
					    // Convert a supported compression method into its relevant u16 stored with little endianness.
 | 
				
			||||||
 | 
					    // https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#445
 | 
				
			||||||
 | 
					    fn from(compression: &Compression) -> u16 {
 | 
				
			||||||
 | 
					        match compression {
 | 
				
			||||||
 | 
					            Compression::Stored => 0,
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					            Compression::Deflate => 8,
 | 
				
			||||||
 | 
					            #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					            Compression::Deflate64 => 9,
 | 
				
			||||||
 | 
					            #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					            Compression::Bz => 12,
 | 
				
			||||||
 | 
					            #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					            Compression::Lzma => 14,
 | 
				
			||||||
 | 
					            #[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					            Compression::Zstd => 93,
 | 
				
			||||||
 | 
					            #[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					            Compression::Xz => 95,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<Compression> for u16 {
 | 
				
			||||||
 | 
					    fn from(compression: Compression) -> u16 {
 | 
				
			||||||
 | 
					        (&compression).into()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Level of compression data should be compressed with for deflate.
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy)]
 | 
				
			||||||
 | 
					pub enum DeflateOption {
 | 
				
			||||||
 | 
					    // Normal (-en) compression option was used.
 | 
				
			||||||
 | 
					    Normal,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Maximum (-exx/-ex) compression option was used.
 | 
				
			||||||
 | 
					    Maximum,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Fast (-ef) compression option was used.
 | 
				
			||||||
 | 
					    Fast,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Super Fast (-es) compression option was used.
 | 
				
			||||||
 | 
					    Super,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Other implementation defined level.
 | 
				
			||||||
 | 
					    Other(i32),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(any(feature = "deflate", feature = "bzip2", feature = "zstd", feature = "lzma", feature = "xz"))]
 | 
				
			||||||
 | 
					impl DeflateOption {
 | 
				
			||||||
 | 
					    pub(crate) fn into_level(self) -> Level {
 | 
				
			||||||
 | 
					        // FIXME: There's no clear documentation on what these specific levels defined in the ZIP specification relate
 | 
				
			||||||
 | 
					        // to. We want to be compatible with any other library, and not specific to `async_compression`'s levels.
 | 
				
			||||||
 | 
					        if let Self::Other(l) = self {
 | 
				
			||||||
 | 
					            Level::Precise(l)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Level::Default
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										44
									
								
								crates/async_zip/src/spec/consts.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								crates/async_zip/src/spec/consts.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const SIGNATURE_LENGTH: usize = 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Local file header constants
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#437
 | 
				
			||||||
 | 
					pub const LFH_SIGNATURE: u32 = 0x4034b50;
 | 
				
			||||||
 | 
					#[allow(dead_code)]
 | 
				
			||||||
 | 
					pub const LFH_LENGTH: usize = 26;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Central directory header constants
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4312
 | 
				
			||||||
 | 
					pub const CDH_SIGNATURE: u32 = 0x2014b50;
 | 
				
			||||||
 | 
					#[allow(dead_code)]
 | 
				
			||||||
 | 
					pub const CDH_LENGTH: usize = 42;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// End of central directory record constants
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4316
 | 
				
			||||||
 | 
					pub const EOCDR_SIGNATURE: u32 = 0x6054b50;
 | 
				
			||||||
 | 
					/// The minimum length of the EOCDR, excluding the signature.
 | 
				
			||||||
 | 
					pub const EOCDR_LENGTH: usize = 18;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The signature for the zip64 end of central directory record.
 | 
				
			||||||
 | 
					/// Ref: https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4314
 | 
				
			||||||
 | 
					pub const ZIP64_EOCDR_SIGNATURE: u32 = 0x06064b50;
 | 
				
			||||||
 | 
					/// The signature for the zip64 end of central directory locator.
 | 
				
			||||||
 | 
					/// Ref: https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4315
 | 
				
			||||||
 | 
					pub const ZIP64_EOCDL_SIGNATURE: u32 = 0x07064b50;
 | 
				
			||||||
 | 
					/// The length of the ZIP64 EOCDL, including the signature.
 | 
				
			||||||
 | 
					/// The EOCDL has a fixed size, thankfully.
 | 
				
			||||||
 | 
					pub const ZIP64_EOCDL_LENGTH: u64 = 20;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The contents of a header field when one must reference the zip64 version instead.
 | 
				
			||||||
 | 
					pub const NON_ZIP64_MAX_SIZE: u32 = 0xFFFFFFFF;
 | 
				
			||||||
 | 
					/// The maximum number of files or disks in a ZIP file before it requires ZIP64.
 | 
				
			||||||
 | 
					pub const NON_ZIP64_MAX_NUM_FILES: u16 = 0xFFFF;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#439
 | 
				
			||||||
 | 
					pub const DATA_DESCRIPTOR_SIGNATURE: u32 = 0x8074b50;
 | 
				
			||||||
 | 
					pub const DATA_DESCRIPTOR_LENGTH: usize = 12;
 | 
				
			||||||
							
								
								
									
										320
									
								
								crates/async_zip/src/spec/extra_field.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								crates/async_zip/src/spec/extra_field.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,320 @@
 | 
				
			||||||
 | 
					// Copyright Cognite AS, 2023
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::error::{Result as ZipResult, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::header::{
 | 
				
			||||||
 | 
					    ExtraField, HeaderId, InfoZipUnicodeCommentExtraField, InfoZipUnicodePathExtraField, UnknownExtraField,
 | 
				
			||||||
 | 
					    Zip64ExtendedInformationExtraField,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::consts::NON_ZIP64_MAX_SIZE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) trait ExtraFieldAsBytes {
 | 
				
			||||||
 | 
					    fn as_bytes(&self) -> Vec<u8>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn count_bytes(&self) -> usize;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ExtraFieldAsBytes for &[ExtraField] {
 | 
				
			||||||
 | 
					    fn as_bytes(&self) -> Vec<u8> {
 | 
				
			||||||
 | 
					        let mut buffer = Vec::new();
 | 
				
			||||||
 | 
					        for field in self.iter() {
 | 
				
			||||||
 | 
					            buffer.append(&mut field.as_bytes());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        buffer
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn count_bytes(&self) -> usize {
 | 
				
			||||||
 | 
					        self.iter().map(|field| field.count_bytes()).sum()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ExtraFieldAsBytes for ExtraField {
 | 
				
			||||||
 | 
					    fn as_bytes(&self) -> Vec<u8> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            ExtraField::Zip64ExtendedInformation(field) => field.as_bytes(),
 | 
				
			||||||
 | 
					            ExtraField::InfoZipUnicodeComment(field) => field.as_bytes(),
 | 
				
			||||||
 | 
					            ExtraField::InfoZipUnicodePath(field) => field.as_bytes(),
 | 
				
			||||||
 | 
					            ExtraField::Unknown(field) => field.as_bytes(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn count_bytes(&self) -> usize {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            ExtraField::Zip64ExtendedInformation(field) => field.count_bytes(),
 | 
				
			||||||
 | 
					            ExtraField::InfoZipUnicodeComment(field) => field.count_bytes(),
 | 
				
			||||||
 | 
					            ExtraField::InfoZipUnicodePath(field) => field.count_bytes(),
 | 
				
			||||||
 | 
					            ExtraField::Unknown(field) => field.count_bytes(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ExtraFieldAsBytes for UnknownExtraField {
 | 
				
			||||||
 | 
					    fn as_bytes(&self) -> Vec<u8> {
 | 
				
			||||||
 | 
					        let mut bytes = Vec::new();
 | 
				
			||||||
 | 
					        let header_id: u16 = self.header_id.into();
 | 
				
			||||||
 | 
					        bytes.append(&mut header_id.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        bytes.append(&mut self.data_size.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        bytes.append(&mut self.content.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bytes
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn count_bytes(&self) -> usize {
 | 
				
			||||||
 | 
					        4 + self.content.len()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ExtraFieldAsBytes for Zip64ExtendedInformationExtraField {
 | 
				
			||||||
 | 
					    fn as_bytes(&self) -> Vec<u8> {
 | 
				
			||||||
 | 
					        let mut bytes = Vec::new();
 | 
				
			||||||
 | 
					        let header_id: u16 = self.header_id.into();
 | 
				
			||||||
 | 
					        bytes.append(&mut header_id.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        bytes.append(&mut (self.content_size() as u16).to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        if let Some(uncompressed_size) = &self.uncompressed_size {
 | 
				
			||||||
 | 
					            bytes.append(&mut uncompressed_size.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(compressed_size) = &self.compressed_size {
 | 
				
			||||||
 | 
					            bytes.append(&mut compressed_size.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(relative_header_offset) = &self.relative_header_offset {
 | 
				
			||||||
 | 
					            bytes.append(&mut relative_header_offset.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(disk_start_number) = &self.disk_start_number {
 | 
				
			||||||
 | 
					            bytes.append(&mut disk_start_number.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bytes
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn count_bytes(&self) -> usize {
 | 
				
			||||||
 | 
					        4 + self.content_size()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ExtraFieldAsBytes for InfoZipUnicodeCommentExtraField {
 | 
				
			||||||
 | 
					    fn as_bytes(&self) -> Vec<u8> {
 | 
				
			||||||
 | 
					        let mut bytes = Vec::new();
 | 
				
			||||||
 | 
					        let header_id: u16 = HeaderId::INFO_ZIP_UNICODE_COMMENT_EXTRA_FIELD.into();
 | 
				
			||||||
 | 
					        bytes.append(&mut header_id.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            InfoZipUnicodeCommentExtraField::V1 { crc32, unicode } => {
 | 
				
			||||||
 | 
					                let data_size: u16 = (5 + unicode.len()).try_into().unwrap();
 | 
				
			||||||
 | 
					                bytes.append(&mut data_size.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					                bytes.push(1);
 | 
				
			||||||
 | 
					                bytes.append(&mut crc32.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					                bytes.append(&mut unicode.clone());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            InfoZipUnicodeCommentExtraField::Unknown { version, data } => {
 | 
				
			||||||
 | 
					                let data_size: u16 = (1 + data.len()).try_into().unwrap();
 | 
				
			||||||
 | 
					                bytes.append(&mut data_size.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					                bytes.push(*version);
 | 
				
			||||||
 | 
					                bytes.append(&mut data.clone());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        bytes
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn count_bytes(&self) -> usize {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            InfoZipUnicodeCommentExtraField::V1 { unicode, .. } => 9 + unicode.len(),
 | 
				
			||||||
 | 
					            InfoZipUnicodeCommentExtraField::Unknown { data, .. } => 5 + data.len(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ExtraFieldAsBytes for InfoZipUnicodePathExtraField {
 | 
				
			||||||
 | 
					    fn as_bytes(&self) -> Vec<u8> {
 | 
				
			||||||
 | 
					        let mut bytes = Vec::new();
 | 
				
			||||||
 | 
					        let header_id: u16 = HeaderId::INFO_ZIP_UNICODE_PATH_EXTRA_FIELD.into();
 | 
				
			||||||
 | 
					        bytes.append(&mut header_id.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            InfoZipUnicodePathExtraField::V1 { crc32, unicode } => {
 | 
				
			||||||
 | 
					                let data_size: u16 = (5 + unicode.len()).try_into().unwrap();
 | 
				
			||||||
 | 
					                bytes.append(&mut data_size.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					                bytes.push(1);
 | 
				
			||||||
 | 
					                bytes.append(&mut crc32.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					                bytes.append(&mut unicode.clone());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            InfoZipUnicodePathExtraField::Unknown { version, data } => {
 | 
				
			||||||
 | 
					                let data_size: u16 = (1 + data.len()).try_into().unwrap();
 | 
				
			||||||
 | 
					                bytes.append(&mut data_size.to_le_bytes().to_vec());
 | 
				
			||||||
 | 
					                bytes.push(*version);
 | 
				
			||||||
 | 
					                bytes.append(&mut data.clone());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        bytes
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn count_bytes(&self) -> usize {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            InfoZipUnicodePathExtraField::V1 { unicode, .. } => 9 + unicode.len(),
 | 
				
			||||||
 | 
					            InfoZipUnicodePathExtraField::Unknown { data, .. } => 5 + data.len(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Parse a zip64 extra field from bytes.
 | 
				
			||||||
 | 
					/// The content of "data" should exclude the header.
 | 
				
			||||||
 | 
					fn zip64_extended_information_field_from_bytes(
 | 
				
			||||||
 | 
					    header_id: HeaderId,
 | 
				
			||||||
 | 
					    data: &[u8],
 | 
				
			||||||
 | 
					    uncompressed_size: u32,
 | 
				
			||||||
 | 
					    compressed_size: u32,
 | 
				
			||||||
 | 
					) -> ZipResult<Zip64ExtendedInformationExtraField> {
 | 
				
			||||||
 | 
					    // slice.take is nightly-only so we'll just use an index to track the current position
 | 
				
			||||||
 | 
					    let mut current_idx = 0;
 | 
				
			||||||
 | 
					    let uncompressed_size = if uncompressed_size == NON_ZIP64_MAX_SIZE && data.len() >= current_idx + 8 {
 | 
				
			||||||
 | 
					        let val = Some(u64::from_le_bytes(data[current_idx..current_idx + 8].try_into().unwrap()));
 | 
				
			||||||
 | 
					        current_idx += 8;
 | 
				
			||||||
 | 
					        val
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let compressed_size = if compressed_size == NON_ZIP64_MAX_SIZE && data.len() >= current_idx + 8 {
 | 
				
			||||||
 | 
					        let val = Some(u64::from_le_bytes(data[current_idx..current_idx + 8].try_into().unwrap()));
 | 
				
			||||||
 | 
					        current_idx += 8;
 | 
				
			||||||
 | 
					        val
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let relative_header_offset = if data.len() >= current_idx + 8 {
 | 
				
			||||||
 | 
					        let val = Some(u64::from_le_bytes(data[current_idx..current_idx + 8].try_into().unwrap()));
 | 
				
			||||||
 | 
					        current_idx += 8;
 | 
				
			||||||
 | 
					        val
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[allow(unused_assignments)]
 | 
				
			||||||
 | 
					    let disk_start_number = if data.len() >= current_idx + 4 {
 | 
				
			||||||
 | 
					        let val = Some(u32::from_le_bytes(data[current_idx..current_idx + 4].try_into().unwrap()));
 | 
				
			||||||
 | 
					        current_idx += 4;
 | 
				
			||||||
 | 
					        val
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(Zip64ExtendedInformationExtraField {
 | 
				
			||||||
 | 
					        header_id,
 | 
				
			||||||
 | 
					        uncompressed_size,
 | 
				
			||||||
 | 
					        compressed_size,
 | 
				
			||||||
 | 
					        relative_header_offset,
 | 
				
			||||||
 | 
					        disk_start_number,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn info_zip_unicode_comment_extra_field_from_bytes(
 | 
				
			||||||
 | 
					    _header_id: HeaderId,
 | 
				
			||||||
 | 
					    data_size: u16,
 | 
				
			||||||
 | 
					    data: &[u8],
 | 
				
			||||||
 | 
					) -> ZipResult<InfoZipUnicodeCommentExtraField> {
 | 
				
			||||||
 | 
					    if data.is_empty() {
 | 
				
			||||||
 | 
					        return Err(ZipError::InfoZipUnicodeCommentFieldIncomplete);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let version = data[0];
 | 
				
			||||||
 | 
					    match version {
 | 
				
			||||||
 | 
					        1 => {
 | 
				
			||||||
 | 
					            if data.len() < 5 {
 | 
				
			||||||
 | 
					                return Err(ZipError::InfoZipUnicodeCommentFieldIncomplete);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            let crc32 = u32::from_le_bytes(data[1..5].try_into().unwrap());
 | 
				
			||||||
 | 
					            let unicode = data[5..(data_size as usize)].to_vec();
 | 
				
			||||||
 | 
					            Ok(InfoZipUnicodeCommentExtraField::V1 { crc32, unicode })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _ => Ok(InfoZipUnicodeCommentExtraField::Unknown { version, data: data[1..(data_size as usize)].to_vec() }),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn info_zip_unicode_path_extra_field_from_bytes(
 | 
				
			||||||
 | 
					    _header_id: HeaderId,
 | 
				
			||||||
 | 
					    data_size: u16,
 | 
				
			||||||
 | 
					    data: &[u8],
 | 
				
			||||||
 | 
					) -> ZipResult<InfoZipUnicodePathExtraField> {
 | 
				
			||||||
 | 
					    if data.is_empty() {
 | 
				
			||||||
 | 
					        return Err(ZipError::InfoZipUnicodePathFieldIncomplete);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let version = data[0];
 | 
				
			||||||
 | 
					    match version {
 | 
				
			||||||
 | 
					        1 => {
 | 
				
			||||||
 | 
					            if data.len() < 5 {
 | 
				
			||||||
 | 
					                return Err(ZipError::InfoZipUnicodePathFieldIncomplete);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            let crc32 = u32::from_le_bytes(data[1..5].try_into().unwrap());
 | 
				
			||||||
 | 
					            let unicode = data[5..(data_size as usize)].to_vec();
 | 
				
			||||||
 | 
					            Ok(InfoZipUnicodePathExtraField::V1 { crc32, unicode })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _ => Ok(InfoZipUnicodePathExtraField::Unknown { version, data: data[1..(data_size as usize)].to_vec() }),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) fn extra_field_from_bytes(
 | 
				
			||||||
 | 
					    header_id: HeaderId,
 | 
				
			||||||
 | 
					    data_size: u16,
 | 
				
			||||||
 | 
					    data: &[u8],
 | 
				
			||||||
 | 
					    uncompressed_size: u32,
 | 
				
			||||||
 | 
					    compressed_size: u32,
 | 
				
			||||||
 | 
					) -> ZipResult<ExtraField> {
 | 
				
			||||||
 | 
					    match header_id {
 | 
				
			||||||
 | 
					        HeaderId::ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD => Ok(ExtraField::Zip64ExtendedInformation(
 | 
				
			||||||
 | 
					            zip64_extended_information_field_from_bytes(header_id, data, uncompressed_size, compressed_size)?,
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					        HeaderId::INFO_ZIP_UNICODE_COMMENT_EXTRA_FIELD => Ok(ExtraField::InfoZipUnicodeComment(
 | 
				
			||||||
 | 
					            info_zip_unicode_comment_extra_field_from_bytes(header_id, data_size, data)?,
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					        HeaderId::INFO_ZIP_UNICODE_PATH_EXTRA_FIELD => Ok(ExtraField::InfoZipUnicodePath(
 | 
				
			||||||
 | 
					            info_zip_unicode_path_extra_field_from_bytes(header_id, data_size, data)?,
 | 
				
			||||||
 | 
					        )),
 | 
				
			||||||
 | 
					        _ => Ok(ExtraField::Unknown(UnknownExtraField { header_id, data_size, content: data.to_vec() })),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Zip64ExtendedInformationExtraFieldBuilder {
 | 
				
			||||||
 | 
					    field: Zip64ExtendedInformationExtraField,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Zip64ExtendedInformationExtraFieldBuilder {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            field: Zip64ExtendedInformationExtraField {
 | 
				
			||||||
 | 
					                header_id: HeaderId::ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD,
 | 
				
			||||||
 | 
					                uncompressed_size: None,
 | 
				
			||||||
 | 
					                compressed_size: None,
 | 
				
			||||||
 | 
					                relative_header_offset: None,
 | 
				
			||||||
 | 
					                disk_start_number: None,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn sizes(mut self, compressed_size: u64, uncompressed_size: u64) -> Self {
 | 
				
			||||||
 | 
					        self.field.compressed_size = Some(compressed_size);
 | 
				
			||||||
 | 
					        self.field.uncompressed_size = Some(uncompressed_size);
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn relative_header_offset(mut self, relative_header_offset: u64) -> Self {
 | 
				
			||||||
 | 
					        self.field.relative_header_offset = Some(relative_header_offset);
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[allow(dead_code)]
 | 
				
			||||||
 | 
					    pub fn disk_start_number(mut self, disk_start_number: u32) -> Self {
 | 
				
			||||||
 | 
					        self.field.disk_start_number = Some(disk_start_number);
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn eof_only(&self) -> bool {
 | 
				
			||||||
 | 
					        (self.field.uncompressed_size.is_none() && self.field.compressed_size.is_none())
 | 
				
			||||||
 | 
					            && (self.field.relative_header_offset.is_some() || self.field.disk_start_number.is_some())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn build(self) -> ZipResult<Zip64ExtendedInformationExtraField> {
 | 
				
			||||||
 | 
					        let field = self.field;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if field.content_size() == 0 {
 | 
				
			||||||
 | 
					            return Err(ZipError::Zip64ExtendedFieldIncomplete);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(field)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										161
									
								
								crates/async_zip/src/spec/header.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								crates/async_zip/src/spec/header.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,161 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#437
 | 
				
			||||||
 | 
					pub struct LocalFileHeader {
 | 
				
			||||||
 | 
					    pub version: u16,
 | 
				
			||||||
 | 
					    pub flags: GeneralPurposeFlag,
 | 
				
			||||||
 | 
					    pub compression: u16,
 | 
				
			||||||
 | 
					    pub mod_time: u16,
 | 
				
			||||||
 | 
					    pub mod_date: u16,
 | 
				
			||||||
 | 
					    pub crc: u32,
 | 
				
			||||||
 | 
					    pub compressed_size: u32,
 | 
				
			||||||
 | 
					    pub uncompressed_size: u32,
 | 
				
			||||||
 | 
					    pub file_name_length: u16,
 | 
				
			||||||
 | 
					    pub extra_field_length: u16,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#444
 | 
				
			||||||
 | 
					#[derive(Copy, Clone)]
 | 
				
			||||||
 | 
					pub struct GeneralPurposeFlag {
 | 
				
			||||||
 | 
					    pub encrypted: bool,
 | 
				
			||||||
 | 
					    pub data_descriptor: bool,
 | 
				
			||||||
 | 
					    pub filename_unicode: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// 2 byte header ids
 | 
				
			||||||
 | 
					/// Ref https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#452
 | 
				
			||||||
 | 
					#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 | 
				
			||||||
 | 
					pub struct HeaderId(pub u16);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl HeaderId {
 | 
				
			||||||
 | 
					    pub const ZIP64_EXTENDED_INFORMATION_EXTRA_FIELD: HeaderId = HeaderId(0x0001);
 | 
				
			||||||
 | 
					    pub const INFO_ZIP_UNICODE_COMMENT_EXTRA_FIELD: HeaderId = HeaderId(0x6375);
 | 
				
			||||||
 | 
					    pub const INFO_ZIP_UNICODE_PATH_EXTRA_FIELD: HeaderId = HeaderId(0x7075);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<u16> for HeaderId {
 | 
				
			||||||
 | 
					    fn from(value: u16) -> Self {
 | 
				
			||||||
 | 
					        HeaderId(value)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<HeaderId> for u16 {
 | 
				
			||||||
 | 
					    fn from(value: HeaderId) -> Self {
 | 
				
			||||||
 | 
					        value.0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Represents each extra field.
 | 
				
			||||||
 | 
					/// Not strictly part of the spec, but is the most useful way to represent the data.
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					#[non_exhaustive]
 | 
				
			||||||
 | 
					pub enum ExtraField {
 | 
				
			||||||
 | 
					    Zip64ExtendedInformation(Zip64ExtendedInformationExtraField),
 | 
				
			||||||
 | 
					    InfoZipUnicodeComment(InfoZipUnicodeCommentExtraField),
 | 
				
			||||||
 | 
					    InfoZipUnicodePath(InfoZipUnicodePathExtraField),
 | 
				
			||||||
 | 
					    Unknown(UnknownExtraField),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// An extended information header for Zip64.
 | 
				
			||||||
 | 
					/// This field is used both for local file headers and central directory records.
 | 
				
			||||||
 | 
					/// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#453
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct Zip64ExtendedInformationExtraField {
 | 
				
			||||||
 | 
					    pub header_id: HeaderId,
 | 
				
			||||||
 | 
					    pub uncompressed_size: Option<u64>,
 | 
				
			||||||
 | 
					    pub compressed_size: Option<u64>,
 | 
				
			||||||
 | 
					    // While not specified in the spec, these two fields are often left out in practice.
 | 
				
			||||||
 | 
					    pub relative_header_offset: Option<u64>,
 | 
				
			||||||
 | 
					    pub disk_start_number: Option<u32>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Zip64ExtendedInformationExtraField {
 | 
				
			||||||
 | 
					    pub(crate) fn content_size(&self) -> usize {
 | 
				
			||||||
 | 
					        self.uncompressed_size.map(|_| 8).unwrap_or_default()
 | 
				
			||||||
 | 
					            + self.compressed_size.map(|_| 8).unwrap_or_default()
 | 
				
			||||||
 | 
					            + self.relative_header_offset.map(|_| 8).unwrap_or_default()
 | 
				
			||||||
 | 
					            + self.disk_start_number.map(|_| 8).unwrap_or_default()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Stores the UTF-8 version of the file comment as stored in the central directory header.
 | 
				
			||||||
 | 
					/// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#468
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub enum InfoZipUnicodeCommentExtraField {
 | 
				
			||||||
 | 
					    V1 { crc32: u32, unicode: Vec<u8> },
 | 
				
			||||||
 | 
					    Unknown { version: u8, data: Vec<u8> },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Stores the UTF-8 version of the file name field as stored in the local header and central directory header.
 | 
				
			||||||
 | 
					/// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#469
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub enum InfoZipUnicodePathExtraField {
 | 
				
			||||||
 | 
					    V1 { crc32: u32, unicode: Vec<u8> },
 | 
				
			||||||
 | 
					    Unknown { version: u8, data: Vec<u8> },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Represents any unparsed extra field.
 | 
				
			||||||
 | 
					#[derive(Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct UnknownExtraField {
 | 
				
			||||||
 | 
					    pub header_id: HeaderId,
 | 
				
			||||||
 | 
					    pub data_size: u16,
 | 
				
			||||||
 | 
					    pub content: Vec<u8>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4312
 | 
				
			||||||
 | 
					pub struct CentralDirectoryRecord {
 | 
				
			||||||
 | 
					    pub v_made_by: u16,
 | 
				
			||||||
 | 
					    pub v_needed: u16,
 | 
				
			||||||
 | 
					    pub flags: GeneralPurposeFlag,
 | 
				
			||||||
 | 
					    pub compression: u16,
 | 
				
			||||||
 | 
					    pub mod_time: u16,
 | 
				
			||||||
 | 
					    pub mod_date: u16,
 | 
				
			||||||
 | 
					    pub crc: u32,
 | 
				
			||||||
 | 
					    pub compressed_size: u32,
 | 
				
			||||||
 | 
					    pub uncompressed_size: u32,
 | 
				
			||||||
 | 
					    pub file_name_length: u16,
 | 
				
			||||||
 | 
					    pub extra_field_length: u16,
 | 
				
			||||||
 | 
					    pub file_comment_length: u16,
 | 
				
			||||||
 | 
					    pub disk_start: u16,
 | 
				
			||||||
 | 
					    pub inter_attr: u16,
 | 
				
			||||||
 | 
					    pub exter_attr: u32,
 | 
				
			||||||
 | 
					    pub lh_offset: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4316
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct EndOfCentralDirectoryHeader {
 | 
				
			||||||
 | 
					    pub(crate) disk_num: u16,
 | 
				
			||||||
 | 
					    pub(crate) start_cent_dir_disk: u16,
 | 
				
			||||||
 | 
					    pub(crate) num_of_entries_disk: u16,
 | 
				
			||||||
 | 
					    pub(crate) num_of_entries: u16,
 | 
				
			||||||
 | 
					    pub(crate) size_cent_dir: u32,
 | 
				
			||||||
 | 
					    pub(crate) cent_dir_offset: u32,
 | 
				
			||||||
 | 
					    pub(crate) file_comm_length: u16,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4314
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq)]
 | 
				
			||||||
 | 
					pub struct Zip64EndOfCentralDirectoryRecord {
 | 
				
			||||||
 | 
					    /// The size of this Zip64EndOfCentralDirectoryRecord.
 | 
				
			||||||
 | 
					    /// This is specified because there is a variable-length extra zip64 information sector.
 | 
				
			||||||
 | 
					    /// However, we will gleefully ignore this sector because it is reserved for use by PKWare.
 | 
				
			||||||
 | 
					    pub size_of_zip64_end_of_cd_record: u64,
 | 
				
			||||||
 | 
					    pub version_made_by: u16,
 | 
				
			||||||
 | 
					    pub version_needed_to_extract: u16,
 | 
				
			||||||
 | 
					    pub disk_number: u32,
 | 
				
			||||||
 | 
					    pub disk_number_start_of_cd: u32,
 | 
				
			||||||
 | 
					    pub num_entries_in_directory_on_disk: u64,
 | 
				
			||||||
 | 
					    pub num_entries_in_directory: u64,
 | 
				
			||||||
 | 
					    pub directory_size: u64,
 | 
				
			||||||
 | 
					    pub offset_of_start_of_directory: u64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#4315
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq)]
 | 
				
			||||||
 | 
					pub struct Zip64EndOfCentralDirectoryLocator {
 | 
				
			||||||
 | 
					    pub number_of_disk_with_start_of_zip64_end_of_central_directory: u32,
 | 
				
			||||||
 | 
					    pub relative_offset: u64,
 | 
				
			||||||
 | 
					    pub total_number_of_disks: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								crates/async_zip/src/spec/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								crates/async_zip/src/spec/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod attribute;
 | 
				
			||||||
 | 
					pub(crate) mod compression;
 | 
				
			||||||
 | 
					pub(crate) mod consts;
 | 
				
			||||||
 | 
					pub(crate) mod extra_field;
 | 
				
			||||||
 | 
					pub(crate) mod header;
 | 
				
			||||||
 | 
					pub(crate) mod parse;
 | 
				
			||||||
 | 
					pub(crate) mod version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use compression::Compression;
 | 
				
			||||||
							
								
								
									
										345
									
								
								crates/async_zip/src/spec/parse.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								crates/async_zip/src/spec/parse.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,345 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::header::{
 | 
				
			||||||
 | 
					    CentralDirectoryRecord, EndOfCentralDirectoryHeader, ExtraField, GeneralPurposeFlag, HeaderId, LocalFileHeader,
 | 
				
			||||||
 | 
					    Zip64EndOfCentralDirectoryLocator, Zip64EndOfCentralDirectoryRecord,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncRead, AsyncReadExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl LocalFileHeader {
 | 
				
			||||||
 | 
					    pub fn as_slice(&self) -> [u8; 26] {
 | 
				
			||||||
 | 
					        let mut array = [0; 26];
 | 
				
			||||||
 | 
					        let mut cursor = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.version.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.flags.as_slice());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.compression.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.mod_time.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.mod_date.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.crc.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.compressed_size.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.uncompressed_size.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.file_name_length.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.extra_field_length.to_le_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl GeneralPurposeFlag {
 | 
				
			||||||
 | 
					    pub fn as_slice(&self) -> [u8; 2] {
 | 
				
			||||||
 | 
					        let encrypted: u16 = match self.encrypted {
 | 
				
			||||||
 | 
					            false => 0x0,
 | 
				
			||||||
 | 
					            true => 0b1,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let data_descriptor: u16 = match self.data_descriptor {
 | 
				
			||||||
 | 
					            false => 0x0,
 | 
				
			||||||
 | 
					            true => 0x8,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let filename_unicode: u16 = match self.filename_unicode {
 | 
				
			||||||
 | 
					            false => 0x0,
 | 
				
			||||||
 | 
					            true => 0x800,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (encrypted | data_descriptor | filename_unicode).to_le_bytes()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl CentralDirectoryRecord {
 | 
				
			||||||
 | 
					    pub fn as_slice(&self) -> [u8; 42] {
 | 
				
			||||||
 | 
					        let mut array = [0; 42];
 | 
				
			||||||
 | 
					        let mut cursor = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.v_made_by.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.v_needed.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.flags.as_slice());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.compression.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.mod_time.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.mod_date.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.crc.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.compressed_size.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.uncompressed_size.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.file_name_length.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.extra_field_length.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.file_comment_length.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.disk_start.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.inter_attr.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.exter_attr.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.lh_offset.to_le_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl EndOfCentralDirectoryHeader {
 | 
				
			||||||
 | 
					    pub fn as_slice(&self) -> [u8; 18] {
 | 
				
			||||||
 | 
					        let mut array = [0; 18];
 | 
				
			||||||
 | 
					        let mut cursor = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.disk_num.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.start_cent_dir_disk.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.num_of_entries_disk.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.num_of_entries.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.size_cent_dir.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.cent_dir_offset.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.file_comm_length.to_le_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<[u8; 26]> for LocalFileHeader {
 | 
				
			||||||
 | 
					    fn from(value: [u8; 26]) -> LocalFileHeader {
 | 
				
			||||||
 | 
					        LocalFileHeader {
 | 
				
			||||||
 | 
					            version: u16::from_le_bytes(value[0..2].try_into().unwrap()),
 | 
				
			||||||
 | 
					            flags: GeneralPurposeFlag::from(u16::from_le_bytes(value[2..4].try_into().unwrap())),
 | 
				
			||||||
 | 
					            compression: u16::from_le_bytes(value[4..6].try_into().unwrap()),
 | 
				
			||||||
 | 
					            mod_time: u16::from_le_bytes(value[6..8].try_into().unwrap()),
 | 
				
			||||||
 | 
					            mod_date: u16::from_le_bytes(value[8..10].try_into().unwrap()),
 | 
				
			||||||
 | 
					            crc: u32::from_le_bytes(value[10..14].try_into().unwrap()),
 | 
				
			||||||
 | 
					            compressed_size: u32::from_le_bytes(value[14..18].try_into().unwrap()),
 | 
				
			||||||
 | 
					            uncompressed_size: u32::from_le_bytes(value[18..22].try_into().unwrap()),
 | 
				
			||||||
 | 
					            file_name_length: u16::from_le_bytes(value[22..24].try_into().unwrap()),
 | 
				
			||||||
 | 
					            extra_field_length: u16::from_le_bytes(value[24..26].try_into().unwrap()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<u16> for GeneralPurposeFlag {
 | 
				
			||||||
 | 
					    fn from(value: u16) -> GeneralPurposeFlag {
 | 
				
			||||||
 | 
					        let encrypted = !matches!(value & 0x1, 0);
 | 
				
			||||||
 | 
					        let data_descriptor = !matches!((value & 0x8) >> 3, 0);
 | 
				
			||||||
 | 
					        let filename_unicode = !matches!((value & 0x800) >> 11, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        GeneralPurposeFlag { encrypted, data_descriptor, filename_unicode }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<[u8; 42]> for CentralDirectoryRecord {
 | 
				
			||||||
 | 
					    fn from(value: [u8; 42]) -> CentralDirectoryRecord {
 | 
				
			||||||
 | 
					        CentralDirectoryRecord {
 | 
				
			||||||
 | 
					            v_made_by: u16::from_le_bytes(value[0..2].try_into().unwrap()),
 | 
				
			||||||
 | 
					            v_needed: u16::from_le_bytes(value[2..4].try_into().unwrap()),
 | 
				
			||||||
 | 
					            flags: GeneralPurposeFlag::from(u16::from_le_bytes(value[4..6].try_into().unwrap())),
 | 
				
			||||||
 | 
					            compression: u16::from_le_bytes(value[6..8].try_into().unwrap()),
 | 
				
			||||||
 | 
					            mod_time: u16::from_le_bytes(value[8..10].try_into().unwrap()),
 | 
				
			||||||
 | 
					            mod_date: u16::from_le_bytes(value[10..12].try_into().unwrap()),
 | 
				
			||||||
 | 
					            crc: u32::from_le_bytes(value[12..16].try_into().unwrap()),
 | 
				
			||||||
 | 
					            compressed_size: u32::from_le_bytes(value[16..20].try_into().unwrap()),
 | 
				
			||||||
 | 
					            uncompressed_size: u32::from_le_bytes(value[20..24].try_into().unwrap()),
 | 
				
			||||||
 | 
					            file_name_length: u16::from_le_bytes(value[24..26].try_into().unwrap()),
 | 
				
			||||||
 | 
					            extra_field_length: u16::from_le_bytes(value[26..28].try_into().unwrap()),
 | 
				
			||||||
 | 
					            file_comment_length: u16::from_le_bytes(value[28..30].try_into().unwrap()),
 | 
				
			||||||
 | 
					            disk_start: u16::from_le_bytes(value[30..32].try_into().unwrap()),
 | 
				
			||||||
 | 
					            inter_attr: u16::from_le_bytes(value[32..34].try_into().unwrap()),
 | 
				
			||||||
 | 
					            exter_attr: u32::from_le_bytes(value[34..38].try_into().unwrap()),
 | 
				
			||||||
 | 
					            lh_offset: u32::from_le_bytes(value[38..42].try_into().unwrap()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<[u8; 18]> for EndOfCentralDirectoryHeader {
 | 
				
			||||||
 | 
					    fn from(value: [u8; 18]) -> EndOfCentralDirectoryHeader {
 | 
				
			||||||
 | 
					        EndOfCentralDirectoryHeader {
 | 
				
			||||||
 | 
					            disk_num: u16::from_le_bytes(value[0..2].try_into().unwrap()),
 | 
				
			||||||
 | 
					            start_cent_dir_disk: u16::from_le_bytes(value[2..4].try_into().unwrap()),
 | 
				
			||||||
 | 
					            num_of_entries_disk: u16::from_le_bytes(value[4..6].try_into().unwrap()),
 | 
				
			||||||
 | 
					            num_of_entries: u16::from_le_bytes(value[6..8].try_into().unwrap()),
 | 
				
			||||||
 | 
					            size_cent_dir: u32::from_le_bytes(value[8..12].try_into().unwrap()),
 | 
				
			||||||
 | 
					            cent_dir_offset: u32::from_le_bytes(value[12..16].try_into().unwrap()),
 | 
				
			||||||
 | 
					            file_comm_length: u16::from_le_bytes(value[16..18].try_into().unwrap()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<[u8; 52]> for Zip64EndOfCentralDirectoryRecord {
 | 
				
			||||||
 | 
					    fn from(value: [u8; 52]) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            size_of_zip64_end_of_cd_record: u64::from_le_bytes(value[0..8].try_into().unwrap()),
 | 
				
			||||||
 | 
					            version_made_by: u16::from_le_bytes(value[8..10].try_into().unwrap()),
 | 
				
			||||||
 | 
					            version_needed_to_extract: u16::from_le_bytes(value[10..12].try_into().unwrap()),
 | 
				
			||||||
 | 
					            disk_number: u32::from_le_bytes(value[12..16].try_into().unwrap()),
 | 
				
			||||||
 | 
					            disk_number_start_of_cd: u32::from_le_bytes(value[16..20].try_into().unwrap()),
 | 
				
			||||||
 | 
					            num_entries_in_directory_on_disk: u64::from_le_bytes(value[20..28].try_into().unwrap()),
 | 
				
			||||||
 | 
					            num_entries_in_directory: u64::from_le_bytes(value[28..36].try_into().unwrap()),
 | 
				
			||||||
 | 
					            directory_size: u64::from_le_bytes(value[36..44].try_into().unwrap()),
 | 
				
			||||||
 | 
					            offset_of_start_of_directory: u64::from_le_bytes(value[44..52].try_into().unwrap()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<[u8; 16]> for Zip64EndOfCentralDirectoryLocator {
 | 
				
			||||||
 | 
					    fn from(value: [u8; 16]) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            number_of_disk_with_start_of_zip64_end_of_central_directory: u32::from_le_bytes(
 | 
				
			||||||
 | 
					                value[0..4].try_into().unwrap(),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            relative_offset: u64::from_le_bytes(value[4..12].try_into().unwrap()),
 | 
				
			||||||
 | 
					            total_number_of_disks: u32::from_le_bytes(value[12..16].try_into().unwrap()),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl LocalFileHeader {
 | 
				
			||||||
 | 
					    pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<LocalFileHeader> {
 | 
				
			||||||
 | 
					        let mut buffer: [u8; 26] = [0; 26];
 | 
				
			||||||
 | 
					        reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					        Ok(LocalFileHeader::from(buffer))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl EndOfCentralDirectoryHeader {
 | 
				
			||||||
 | 
					    pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<EndOfCentralDirectoryHeader> {
 | 
				
			||||||
 | 
					        let mut buffer: [u8; 18] = [0; 18];
 | 
				
			||||||
 | 
					        reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					        Ok(EndOfCentralDirectoryHeader::from(buffer))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl CentralDirectoryRecord {
 | 
				
			||||||
 | 
					    pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<CentralDirectoryRecord> {
 | 
				
			||||||
 | 
					        let mut buffer: [u8; 42] = [0; 42];
 | 
				
			||||||
 | 
					        reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					        Ok(CentralDirectoryRecord::from(buffer))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Zip64EndOfCentralDirectoryRecord {
 | 
				
			||||||
 | 
					    pub async fn from_reader<R: AsyncRead + Unpin>(reader: &mut R) -> Result<Zip64EndOfCentralDirectoryRecord> {
 | 
				
			||||||
 | 
					        let mut buffer: [u8; 52] = [0; 52];
 | 
				
			||||||
 | 
					        reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					        Ok(Self::from(buffer))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn as_bytes(&self) -> [u8; 52] {
 | 
				
			||||||
 | 
					        let mut array = [0; 52];
 | 
				
			||||||
 | 
					        let mut cursor = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.size_of_zip64_end_of_cd_record.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.version_made_by.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.version_needed_to_extract.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.disk_number.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.disk_number_start_of_cd.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.num_entries_in_directory_on_disk.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.num_entries_in_directory.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.directory_size.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.offset_of_start_of_directory.to_le_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Zip64EndOfCentralDirectoryLocator {
 | 
				
			||||||
 | 
					    /// Read 4 bytes from the reader and check whether its signature matches that of the EOCDL.
 | 
				
			||||||
 | 
					    /// If it does, return Some(EOCDL), otherwise return None.
 | 
				
			||||||
 | 
					    pub async fn try_from_reader<R: AsyncRead + Unpin>(
 | 
				
			||||||
 | 
					        reader: &mut R,
 | 
				
			||||||
 | 
					    ) -> Result<Option<Zip64EndOfCentralDirectoryLocator>> {
 | 
				
			||||||
 | 
					        let signature = {
 | 
				
			||||||
 | 
					            let mut buffer = [0; 4];
 | 
				
			||||||
 | 
					            reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					            u32::from_le_bytes(buffer)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        if signature != ZIP64_EOCDL_SIGNATURE {
 | 
				
			||||||
 | 
					            return Ok(None);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let mut buffer: [u8; 16] = [0; 16];
 | 
				
			||||||
 | 
					        reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					        Ok(Some(Self::from(buffer)))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn as_bytes(&self) -> [u8; 16] {
 | 
				
			||||||
 | 
					        let mut array = [0; 16];
 | 
				
			||||||
 | 
					        let mut cursor = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.number_of_disk_with_start_of_zip64_end_of_central_directory.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.relative_offset.to_le_bytes());
 | 
				
			||||||
 | 
					        array_push!(array, cursor, self.total_number_of_disks.to_le_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        array
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Parse the extra fields.
 | 
				
			||||||
 | 
					pub fn parse_extra_fields(data: Vec<u8>, uncompressed_size: u32, compressed_size: u32) -> Result<Vec<ExtraField>> {
 | 
				
			||||||
 | 
					    let mut cursor = 0;
 | 
				
			||||||
 | 
					    let mut extra_fields = Vec::new();
 | 
				
			||||||
 | 
					    while cursor + 4 < data.len() {
 | 
				
			||||||
 | 
					        let header_id: HeaderId = u16::from_le_bytes(data[cursor..cursor + 2].try_into().unwrap()).into();
 | 
				
			||||||
 | 
					        let field_size = u16::from_le_bytes(data[cursor + 2..cursor + 4].try_into().unwrap());
 | 
				
			||||||
 | 
					        if cursor + 4 + field_size as usize > data.len() {
 | 
				
			||||||
 | 
					            return Err(ZipError::InvalidExtraFieldHeader(field_size, data.len() - cursor - 8 - field_size as usize));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let data = &data[cursor + 4..cursor + 4 + field_size as usize];
 | 
				
			||||||
 | 
					        extra_fields.push(extra_field_from_bytes(header_id, field_size, data, uncompressed_size, compressed_size)?);
 | 
				
			||||||
 | 
					        cursor += 4 + field_size as usize;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Ok(extra_fields)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Replace elements of an array at a given cursor index for use with a zero-initialised array.
 | 
				
			||||||
 | 
					macro_rules! array_push {
 | 
				
			||||||
 | 
					    ($arr:ident, $cursor:ident, $value:expr) => {{
 | 
				
			||||||
 | 
					        for entry in $value {
 | 
				
			||||||
 | 
					            $arr[$cursor] = entry;
 | 
				
			||||||
 | 
					            $cursor += 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::spec::consts::ZIP64_EOCDL_SIGNATURE;
 | 
				
			||||||
 | 
					use crate::spec::extra_field::extra_field_from_bytes;
 | 
				
			||||||
 | 
					pub(crate) use array_push;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn test_parse_zip64_eocdr() {
 | 
				
			||||||
 | 
					        let eocdr: [u8; 56] = [
 | 
				
			||||||
 | 
					            0x50, 0x4B, 0x06, 0x06, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x03, 0x2D, 0x00, 0x00, 0x00,
 | 
				
			||||||
 | 
					            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
 | 
				
			||||||
 | 
					            0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
				
			||||||
 | 
					            0x00, 0x00,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let without_signature: [u8; 52] = eocdr[4..56].try_into().unwrap();
 | 
				
			||||||
 | 
					        let zip64eocdr = Zip64EndOfCentralDirectoryRecord::from(without_signature);
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            zip64eocdr,
 | 
				
			||||||
 | 
					            Zip64EndOfCentralDirectoryRecord {
 | 
				
			||||||
 | 
					                size_of_zip64_end_of_cd_record: 44,
 | 
				
			||||||
 | 
					                version_made_by: 798,
 | 
				
			||||||
 | 
					                version_needed_to_extract: 45,
 | 
				
			||||||
 | 
					                disk_number: 0,
 | 
				
			||||||
 | 
					                disk_number_start_of_cd: 0,
 | 
				
			||||||
 | 
					                num_entries_in_directory_on_disk: 1,
 | 
				
			||||||
 | 
					                num_entries_in_directory: 1,
 | 
				
			||||||
 | 
					                directory_size: 47,
 | 
				
			||||||
 | 
					                offset_of_start_of_directory: 64,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn test_parse_zip64_eocdl() {
 | 
				
			||||||
 | 
					        let eocdl: [u8; 20] = [
 | 
				
			||||||
 | 
					            0x50, 0x4B, 0x06, 0x07, 0x00, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
 | 
				
			||||||
 | 
					            0x00, 0x00,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        let mut cursor = futures_lite::io::Cursor::new(eocdl);
 | 
				
			||||||
 | 
					        let zip64eocdl = Zip64EndOfCentralDirectoryLocator::try_from_reader(&mut cursor).await.unwrap().unwrap();
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            zip64eocdl,
 | 
				
			||||||
 | 
					            Zip64EndOfCentralDirectoryLocator {
 | 
				
			||||||
 | 
					                number_of_disk_with_start_of_zip64_end_of_central_directory: 0,
 | 
				
			||||||
 | 
					                relative_offset: 111,
 | 
				
			||||||
 | 
					                total_number_of_disks: 1,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								crates/async_zip/src/spec/version.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								crates/async_zip/src/spec/version.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2021 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::entry::ZipEntry;
 | 
				
			||||||
 | 
					#[cfg(any(
 | 
				
			||||||
 | 
					    feature = "deflate",
 | 
				
			||||||
 | 
					    feature = "bzip2",
 | 
				
			||||||
 | 
					    feature = "zstd",
 | 
				
			||||||
 | 
					    feature = "lzma",
 | 
				
			||||||
 | 
					    feature = "xz",
 | 
				
			||||||
 | 
					    feature = "deflate64"
 | 
				
			||||||
 | 
					))]
 | 
				
			||||||
 | 
					use crate::spec::Compression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) const SPEC_VERSION_MADE_BY: u16 = 63;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#443
 | 
				
			||||||
 | 
					pub fn as_needed_to_extract(entry: &ZipEntry) -> u16 {
 | 
				
			||||||
 | 
					    let mut version = match entry.compression() {
 | 
				
			||||||
 | 
					        #[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					        Compression::Deflate => 20,
 | 
				
			||||||
 | 
					        #[cfg(feature = "deflate64")]
 | 
				
			||||||
 | 
					        Compression::Deflate64 => 21,
 | 
				
			||||||
 | 
					        #[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					        Compression::Bz => 46,
 | 
				
			||||||
 | 
					        #[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					        Compression::Lzma => 63,
 | 
				
			||||||
 | 
					        _ => 10,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if let Ok(true) = entry.dir() {
 | 
				
			||||||
 | 
					        version = std::cmp::max(version, 20);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    version
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://github.com/Majored/rs-async-zip/blob/main/SPECIFICATION.md#442
 | 
				
			||||||
 | 
					pub fn as_made_by() -> u16 {
 | 
				
			||||||
 | 
					    // Default to UNIX mapping for the moment.
 | 
				
			||||||
 | 
					    3 << 8 | SPEC_VERSION_MADE_BY
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										112
									
								
								crates/async_zip/src/string.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								crates/async_zip/src/string.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,112 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A string encoding supported by this crate.
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy)]
 | 
				
			||||||
 | 
					pub enum StringEncoding {
 | 
				
			||||||
 | 
					    Utf8,
 | 
				
			||||||
 | 
					    Raw,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A string wrapper for handling different encodings.
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct ZipString {
 | 
				
			||||||
 | 
					    encoding: StringEncoding,
 | 
				
			||||||
 | 
					    raw: Vec<u8>,
 | 
				
			||||||
 | 
					    alternative: Option<Vec<u8>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipString {
 | 
				
			||||||
 | 
					    /// Constructs a new encoded string from its raw bytes and its encoding type.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// # Note
 | 
				
			||||||
 | 
					    /// If the provided encoding is [`StringEncoding::Utf8`] but the raw bytes are not valid UTF-8 (ie. a call to
 | 
				
			||||||
 | 
					    /// `std::str::from_utf8()` fails), the encoding is defaulted back to [`StringEncoding::Raw`].
 | 
				
			||||||
 | 
					    pub fn new(raw: Vec<u8>, mut encoding: StringEncoding) -> Self {
 | 
				
			||||||
 | 
					        if let StringEncoding::Utf8 = encoding {
 | 
				
			||||||
 | 
					            if std::str::from_utf8(&raw).is_err() {
 | 
				
			||||||
 | 
					                encoding = StringEncoding::Raw;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self { encoding, raw, alternative: None }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Constructs a new encoded string from utf-8 data, with an alternative in native MBCS encoding.
 | 
				
			||||||
 | 
					    pub fn new_with_alternative(utf8: String, alternative: Vec<u8>) -> Self {
 | 
				
			||||||
 | 
					        Self { encoding: StringEncoding::Utf8, raw: utf8.into_bytes(), alternative: Some(alternative) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the raw bytes for this string.
 | 
				
			||||||
 | 
					    pub fn as_bytes(&self) -> &[u8] {
 | 
				
			||||||
 | 
					        &self.raw
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the encoding type for this string.
 | 
				
			||||||
 | 
					    pub fn encoding(&self) -> StringEncoding {
 | 
				
			||||||
 | 
					        self.encoding
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the alternative bytes (in native MBCS encoding) for this string.
 | 
				
			||||||
 | 
					    pub fn alternative(&self) -> Option<&[u8]> {
 | 
				
			||||||
 | 
					        self.alternative.as_deref()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the raw bytes converted into a string slice.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// # Note
 | 
				
			||||||
 | 
					    /// A call to this method will only succeed if the encoding type is [`StringEncoding::Utf8`].
 | 
				
			||||||
 | 
					    pub fn as_str(&self) -> Result<&str> {
 | 
				
			||||||
 | 
					        if !matches!(self.encoding, StringEncoding::Utf8) {
 | 
				
			||||||
 | 
					            return Err(ZipError::StringNotUtf8);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // SAFETY:
 | 
				
			||||||
 | 
					        // "The bytes passed in must be valid UTF-8.'
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // This function will error if self.encoding is not StringEncoding::Utf8.
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // self.encoding is only ever StringEncoding::Utf8 if this variant was provided to the constructor AND the
 | 
				
			||||||
 | 
					        // call to `std::str::from_utf8()` within the constructor succeeded. Mutable access to the inner vector is
 | 
				
			||||||
 | 
					        // never given and no method implemented on this type mutates the inner vector.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(unsafe { std::str::from_utf8_unchecked(&self.raw) })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the raw bytes converted to an owned string.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// # Note
 | 
				
			||||||
 | 
					    /// A call to this method will only succeed if the encoding type is [`StringEncoding::Utf8`].
 | 
				
			||||||
 | 
					    pub fn into_string(self) -> Result<String> {
 | 
				
			||||||
 | 
					        if !matches!(self.encoding, StringEncoding::Utf8) {
 | 
				
			||||||
 | 
					            return Err(ZipError::StringNotUtf8);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // SAFETY: See above.
 | 
				
			||||||
 | 
					        Ok(unsafe { String::from_utf8_unchecked(self.raw) })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the alternative bytes (in native MBCS encoding) converted to the owned.
 | 
				
			||||||
 | 
					    pub fn into_alternative(self) -> Option<Vec<u8>> {
 | 
				
			||||||
 | 
					        self.alternative
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns whether this string is encoded as utf-8 without an alternative.
 | 
				
			||||||
 | 
					    pub fn is_utf8_without_alternative(&self) -> bool {
 | 
				
			||||||
 | 
					        matches!(self.encoding, StringEncoding::Utf8) && self.alternative.is_none()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<String> for ZipString {
 | 
				
			||||||
 | 
					    fn from(value: String) -> Self {
 | 
				
			||||||
 | 
					        Self { encoding: StringEncoding::Utf8, raw: value.into_bytes(), alternative: None }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<&str> for ZipString {
 | 
				
			||||||
 | 
					    fn from(value: &str) -> Self {
 | 
				
			||||||
 | 
					        Self { encoding: StringEncoding::Utf8, raw: value.as_bytes().to_vec(), alternative: None }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								crates/async_zip/src/tests/combined/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								crates/async_zip/src/tests/combined/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
							
								
								
									
										16
									
								
								crates/async_zip/src/tests/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								crates/async_zip/src/tests/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod combined;
 | 
				
			||||||
 | 
					pub(crate) mod read;
 | 
				
			||||||
 | 
					pub(crate) mod spec;
 | 
				
			||||||
 | 
					pub(crate) mod write;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::sync::Once;
 | 
				
			||||||
 | 
					static ENV_LOGGER: Once = Once::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Initialize the env logger for any tests that require it.
 | 
				
			||||||
 | 
					/// Safe to call multiple times.
 | 
				
			||||||
 | 
					fn init_logger() {
 | 
				
			||||||
 | 
					    ENV_LOGGER.call_once(|| env_logger::Builder::from_default_env().format_module_path(true).init());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/bzip2.data
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/bzip2.data
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/deflate.data
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/deflate.data
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/lzma.data
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/lzma.data
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										46
									
								
								crates/async_zip/src/tests/read/compression/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								crates/async_zip/src/tests/read/compression/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::compressed::CompressedReader;
 | 
				
			||||||
 | 
					use crate::spec::Compression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					compressed_test_helper!(stored_test, Compression::Stored, "foo bar", "foo bar");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					compressed_test_helper!(deflate_test, Compression::Deflate, "foo bar", include_bytes!("deflate.data"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "bzip2")]
 | 
				
			||||||
 | 
					compressed_test_helper!(bz_test, Compression::Bz, "foo bar", include_bytes!("bzip2.data"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "lzma")]
 | 
				
			||||||
 | 
					compressed_test_helper!(lzma_test, Compression::Lzma, "foo bar", include_bytes!("lzma.data"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					compressed_test_helper!(zstd_test, Compression::Zstd, "foo bar", include_bytes!("zstd.data"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "xz")]
 | 
				
			||||||
 | 
					compressed_test_helper!(xz_test, Compression::Xz, "foo bar", include_bytes!("xz.data"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A helper macro for generating a CompressedReader test using a specific compression method.
 | 
				
			||||||
 | 
					macro_rules! compressed_test_helper {
 | 
				
			||||||
 | 
					    ($name:ident, $typ:expr, $data_raw:expr, $data:expr) => {
 | 
				
			||||||
 | 
					        #[cfg(test)]
 | 
				
			||||||
 | 
					        #[tokio::test]
 | 
				
			||||||
 | 
					        async fn $name() {
 | 
				
			||||||
 | 
					            use futures_lite::io::{AsyncReadExt, Cursor};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let data = $data;
 | 
				
			||||||
 | 
					            let data_raw = $data_raw;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let cursor = Cursor::new(data);
 | 
				
			||||||
 | 
					            let mut reader = CompressedReader::new(cursor, $typ);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let mut read_data = String::new();
 | 
				
			||||||
 | 
					            reader.read_to_string(&mut read_data).await.expect("read into CompressedReader failed");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            assert_eq!(read_data, data_raw);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use compressed_test_helper;
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/xz.data
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/xz.data
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/zstd.data
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/compression/zstd.data
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/locator/empty.zip
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/locator/empty.zip
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										64
									
								
								crates/async_zip/src/tests/read/locator/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								crates/async_zip/src/tests/read/locator/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn search_one_byte_test() {
 | 
				
			||||||
 | 
					    let buffer: &[u8] = &[0x0, 0x0, 0x0, 0x0, 0x0, 0x0];
 | 
				
			||||||
 | 
					    let signature: &[u8] = &[0x1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let matched = crate::base::read::io::locator::reverse_search_buffer(buffer, signature);
 | 
				
			||||||
 | 
					    assert!(matched.is_none());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let buffer: &[u8] = &[0x2, 0x1, 0x0, 0x0, 0x0, 0x0];
 | 
				
			||||||
 | 
					    let signature: &[u8] = &[0x1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let matched = crate::base::read::io::locator::reverse_search_buffer(buffer, signature);
 | 
				
			||||||
 | 
					    assert!(matched.is_some());
 | 
				
			||||||
 | 
					    assert_eq!(1, matched.unwrap());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn search_two_byte_test() {
 | 
				
			||||||
 | 
					    let buffer: &[u8] = &[0x2, 0x1, 0x0, 0x0, 0x0, 0x0];
 | 
				
			||||||
 | 
					    let signature: &[u8] = &[0x2, 0x1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let matched = crate::base::read::io::locator::reverse_search_buffer(buffer, signature);
 | 
				
			||||||
 | 
					    assert!(matched.is_some());
 | 
				
			||||||
 | 
					    assert_eq!(1, matched.unwrap());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn locator_empty_test() {
 | 
				
			||||||
 | 
					    use futures_lite::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let data = &include_bytes!("empty.zip");
 | 
				
			||||||
 | 
					    let mut cursor = Cursor::new(data);
 | 
				
			||||||
 | 
					    let eocdr = crate::base::read::io::locator::eocdr(&mut cursor).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(eocdr.is_ok());
 | 
				
			||||||
 | 
					    assert_eq!(eocdr.unwrap(), 4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn locator_empty_max_comment_test() {
 | 
				
			||||||
 | 
					    use futures_lite::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let data = &include_bytes!("empty-with-max-comment.zip");
 | 
				
			||||||
 | 
					    let mut cursor = Cursor::new(data);
 | 
				
			||||||
 | 
					    let eocdr = crate::base::read::io::locator::eocdr(&mut cursor).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(eocdr.is_ok());
 | 
				
			||||||
 | 
					    assert_eq!(eocdr.unwrap(), 4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn locator_buffer_boundary_test() {
 | 
				
			||||||
 | 
					    use futures_lite::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let data = &include_bytes!("empty-buffer-boundary.zip");
 | 
				
			||||||
 | 
					    let mut cursor = Cursor::new(data);
 | 
				
			||||||
 | 
					    let eocdr = crate::base::read::io::locator::eocdr(&mut cursor).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(eocdr.is_ok());
 | 
				
			||||||
 | 
					    assert_eq!(eocdr.unwrap(), 4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								crates/async_zip/src/tests/read/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								crates/async_zip/src/tests/read/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod compression;
 | 
				
			||||||
 | 
					pub(crate) mod locator;
 | 
				
			||||||
 | 
					pub(crate) mod zip64;
 | 
				
			||||||
							
								
								
									
										107
									
								
								crates/async_zip/src/tests/read/zip64/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								crates/async_zip/src/tests/read/zip64/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,107 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Cognite AS
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::tests::init_logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ZIP64_ZIP_CONTENTS: &str = "Hello World!\n";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Tests opening and reading a zip64 archive.
 | 
				
			||||||
 | 
					/// It contains one file named "-" with a zip 64 extended field header.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_read_zip64_archive_mem() {
 | 
				
			||||||
 | 
					    use crate::base::read::mem::ZipFileReader;
 | 
				
			||||||
 | 
					    init_logger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let data = include_bytes!("zip64.zip").to_vec();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reader = ZipFileReader::new(data).await.unwrap();
 | 
				
			||||||
 | 
					    let mut entry_reader = reader.reader_without_entry(0).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut read_data = String::new();
 | 
				
			||||||
 | 
					    entry_reader.read_to_string(&mut read_data).await.expect("read failed");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert_eq!(
 | 
				
			||||||
 | 
					        read_data.chars().count(),
 | 
				
			||||||
 | 
					        ZIP64_ZIP_CONTENTS.chars().count(),
 | 
				
			||||||
 | 
					        "{read_data:?} != {ZIP64_ZIP_CONTENTS:?}"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    assert_eq!(read_data, ZIP64_ZIP_CONTENTS);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Like test_read_zip64_archive_mem() but for the streaming version
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_read_zip64_archive_stream() {
 | 
				
			||||||
 | 
					    use crate::base::read::stream::ZipFileReader;
 | 
				
			||||||
 | 
					    init_logger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let data = include_bytes!("zip64.zip").to_vec();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reader = ZipFileReader::new(data.as_slice());
 | 
				
			||||||
 | 
					    let mut entry_reader = reader.next_without_entry().await.unwrap().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut read_data = String::new();
 | 
				
			||||||
 | 
					    entry_reader.reader_mut().read_to_string(&mut read_data).await.expect("read failed");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert_eq!(
 | 
				
			||||||
 | 
					        read_data.chars().count(),
 | 
				
			||||||
 | 
					        ZIP64_ZIP_CONTENTS.chars().count(),
 | 
				
			||||||
 | 
					        "{read_data:?} != {ZIP64_ZIP_CONTENTS:?}"
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    assert_eq!(read_data, ZIP64_ZIP_CONTENTS);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Generate an example file only if it doesn't exist already.
 | 
				
			||||||
 | 
					/// The file is placed adjacent to this rs file.
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio")]
 | 
				
			||||||
 | 
					fn generate_zip64many_zip() -> std::path::PathBuf {
 | 
				
			||||||
 | 
					    use std::io::Write;
 | 
				
			||||||
 | 
					    use zip::write::FileOptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
 | 
				
			||||||
 | 
					    path.push("src/tests/read/zip64/zip64many.zip");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Only recreate the zip if it doesnt already exist.
 | 
				
			||||||
 | 
					    if path.exists() {
 | 
				
			||||||
 | 
					        return path;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let zip_file = std::fs::File::create(&path).unwrap();
 | 
				
			||||||
 | 
					    let mut zip = zip::ZipWriter::new(zip_file);
 | 
				
			||||||
 | 
					    let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for i in 0..2_u32.pow(16) + 1 {
 | 
				
			||||||
 | 
					        zip.start_file(format!("{i}.txt"), options).unwrap();
 | 
				
			||||||
 | 
					        zip.write_all(b"\n").unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    zip.finish().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    path
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test reading a generated zip64 archive that contains more than 2^16 entries.
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio-fs")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_read_zip64_archive_many_entries() {
 | 
				
			||||||
 | 
					    use crate::tokio::read::fs::ZipFileReader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init_logger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let path = generate_zip64many_zip();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reader = ZipFileReader::new(path).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Verify that each entry exists and is has the contents "\n"
 | 
				
			||||||
 | 
					    for i in 0..2_u32.pow(16) + 1 {
 | 
				
			||||||
 | 
					        let entry = reader.file().entries().get(i as usize).unwrap();
 | 
				
			||||||
 | 
					        eprintln!("{:?}", entry.filename().as_bytes());
 | 
				
			||||||
 | 
					        assert_eq!(entry.filename.as_str().unwrap(), format!("{i}.txt"));
 | 
				
			||||||
 | 
					        let mut entry = reader.reader_without_entry(i as usize).await.unwrap();
 | 
				
			||||||
 | 
					        let mut contents = String::new();
 | 
				
			||||||
 | 
					        entry.read_to_string(&mut contents).await.unwrap();
 | 
				
			||||||
 | 
					        assert_eq!(contents, "\n");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/zip64/zip64.zip
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/src/tests/read/zip64/zip64.zip
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										44
									
								
								crates/async_zip/src/tests/spec/date.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								crates/async_zip/src/tests/spec/date.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					use chrono::{TimeZone, Utc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::ZipDateTimeBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					#[cfg(feature = "chrono")]
 | 
				
			||||||
 | 
					fn date_conversion_test_chrono() {
 | 
				
			||||||
 | 
					    let original_dt = Utc.timestamp_opt(1666544102, 0).unwrap();
 | 
				
			||||||
 | 
					    let zip_dt = crate::ZipDateTime::from_chrono(&original_dt);
 | 
				
			||||||
 | 
					    let result_dt = zip_dt.as_chrono().single().expect("expected single unique result");
 | 
				
			||||||
 | 
					    assert_eq!(result_dt, original_dt);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn date_conversion_test() {
 | 
				
			||||||
 | 
					    let year = 2000;
 | 
				
			||||||
 | 
					    let month = 9;
 | 
				
			||||||
 | 
					    let day = 8;
 | 
				
			||||||
 | 
					    let hour = 7;
 | 
				
			||||||
 | 
					    let minute = 5;
 | 
				
			||||||
 | 
					    let second = 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut builder = ZipDateTimeBuilder::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    builder = builder.year(year);
 | 
				
			||||||
 | 
					    builder = builder.month(month);
 | 
				
			||||||
 | 
					    builder = builder.day(day);
 | 
				
			||||||
 | 
					    builder = builder.hour(hour);
 | 
				
			||||||
 | 
					    builder = builder.minute(minute);
 | 
				
			||||||
 | 
					    builder = builder.second(second);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let built = builder.build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert_eq!(year, built.year());
 | 
				
			||||||
 | 
					    assert_eq!(month, built.month());
 | 
				
			||||||
 | 
					    assert_eq!(day, built.day());
 | 
				
			||||||
 | 
					    assert_eq!(hour, built.hour());
 | 
				
			||||||
 | 
					    assert_eq!(minute, built.minute());
 | 
				
			||||||
 | 
					    assert_eq!(second, built.second());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								crates/async_zip/src/tests/spec/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								crates/async_zip/src/tests/spec/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod date;
 | 
				
			||||||
							
								
								
									
										29
									
								
								crates/async_zip/src/tests/write/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								crates/async_zip/src/tests/write/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncWrite;
 | 
				
			||||||
 | 
					use std::io::Error;
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					use std::task::{Context, Poll};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) mod offset;
 | 
				
			||||||
 | 
					mod zip64;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// /dev/null for AsyncWrite.
 | 
				
			||||||
 | 
					/// Useful for tests that involve writing, but not reading, large amounts of data.
 | 
				
			||||||
 | 
					pub(crate) struct AsyncSink;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AsyncSink is always ready to receive bytes and throw them away.
 | 
				
			||||||
 | 
					impl AsyncWrite for AsyncSink {
 | 
				
			||||||
 | 
					    fn poll_write(self: Pin<&mut Self>, _: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, Error>> {
 | 
				
			||||||
 | 
					        Poll::Ready(Ok(buf.len()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Error>> {
 | 
				
			||||||
 | 
					        Poll::Ready(Ok(()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Error>> {
 | 
				
			||||||
 | 
					        Poll::Ready(Ok(()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								crates/async_zip/src/tests/write/offset/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								crates/async_zip/src/tests/write/offset/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::write::io::offset::AsyncOffsetWriter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn basic() {
 | 
				
			||||||
 | 
					    use futures_lite::io::AsyncWriteExt;
 | 
				
			||||||
 | 
					    use futures_lite::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut writer = AsyncOffsetWriter::new(Cursor::new(Vec::new()));
 | 
				
			||||||
 | 
					    assert_eq!(writer.offset(), 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    writer.write_all(b"Foo. Bar. Foo. Bar.").await.expect("failed to write data");
 | 
				
			||||||
 | 
					    assert_eq!(writer.offset(), 19);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    writer.write_all(b"Foo. Foo.").await.expect("failed to write data");
 | 
				
			||||||
 | 
					    assert_eq!(writer.offset(), 28);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    writer.write_all(b"Bar. Bar.").await.expect("failed to write data");
 | 
				
			||||||
 | 
					    assert_eq!(writer.offset(), 37);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										243
									
								
								crates/async_zip/src/tests/write/zip64/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								crates/async_zip/src/tests/write/zip64/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,243 @@
 | 
				
			||||||
 | 
					// Copyright Cognite AS, 2023
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::write::ZipFileWriter;
 | 
				
			||||||
 | 
					use crate::error::{Zip64ErrorCase, ZipError};
 | 
				
			||||||
 | 
					use crate::spec::consts::NON_ZIP64_MAX_SIZE;
 | 
				
			||||||
 | 
					use crate::tests::init_logger;
 | 
				
			||||||
 | 
					use crate::tests::write::AsyncSink;
 | 
				
			||||||
 | 
					use crate::{Compression, ZipEntryBuilder};
 | 
				
			||||||
 | 
					use std::io::Read;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::spec::header::ExtraField;
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncWriteExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Useful constants for writing a large file.
 | 
				
			||||||
 | 
					const BATCH_SIZE: usize = 100_000;
 | 
				
			||||||
 | 
					const NUM_BATCHES: usize = NON_ZIP64_MAX_SIZE as usize / BATCH_SIZE + 1;
 | 
				
			||||||
 | 
					const BATCHED_FILE_SIZE: usize = NUM_BATCHES * BATCH_SIZE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test writing a small zip64 file.
 | 
				
			||||||
 | 
					/// No zip64 extra fields will be emitted for EntryWhole.
 | 
				
			||||||
 | 
					/// Z64 end of directory record & locator should be emitted
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_write_zip64_file() {
 | 
				
			||||||
 | 
					    init_logger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut buffer = Vec::new();
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut buffer).force_zip64();
 | 
				
			||||||
 | 
					    let entry = ZipEntryBuilder::new("file1".to_string().into(), Compression::Stored);
 | 
				
			||||||
 | 
					    writer.write_entry_whole(entry, &[0, 0, 0, 0]).await.unwrap();
 | 
				
			||||||
 | 
					    let entry = ZipEntryBuilder::new("file2".to_string().into(), Compression::Stored);
 | 
				
			||||||
 | 
					    let mut entry_writer = writer.write_entry_stream(entry).await.unwrap();
 | 
				
			||||||
 | 
					    entry_writer.write_all(&[0, 0, 0, 0]).await.unwrap();
 | 
				
			||||||
 | 
					    entry_writer.close().await.unwrap();
 | 
				
			||||||
 | 
					    writer.close().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let cursor = std::io::Cursor::new(buffer);
 | 
				
			||||||
 | 
					    let mut zip = zip::read::ZipArchive::new(cursor).unwrap();
 | 
				
			||||||
 | 
					    let mut file1 = zip.by_name("file1").unwrap();
 | 
				
			||||||
 | 
					    assert_eq!(file1.extra_data(), &[] as &[u8]);
 | 
				
			||||||
 | 
					    let mut buffer = Vec::new();
 | 
				
			||||||
 | 
					    file1.read_to_end(&mut buffer).unwrap();
 | 
				
			||||||
 | 
					    assert_eq!(buffer.as_slice(), &[0, 0, 0, 0]);
 | 
				
			||||||
 | 
					    drop(file1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut file2 = zip.by_name("file2").unwrap();
 | 
				
			||||||
 | 
					    let mut buffer = Vec::new();
 | 
				
			||||||
 | 
					    file2.read_to_end(&mut buffer).unwrap();
 | 
				
			||||||
 | 
					    assert_eq!(buffer.as_slice(), &[0, 0, 0, 0]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test writing a large zip64 file. This test will use upwards of 4GB of memory.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_write_large_zip64_file() {
 | 
				
			||||||
 | 
					    init_logger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Allocate space with some extra for metadata records
 | 
				
			||||||
 | 
					    let mut buffer = Vec::with_capacity(BATCHED_FILE_SIZE + 100_000);
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Stream-written zip files are dubiously spec-conformant. We need to specify a valid file size
 | 
				
			||||||
 | 
					    // in order for rs-zip (and unzip) to correctly read these files.
 | 
				
			||||||
 | 
					    let entry = ZipEntryBuilder::new("file".to_string().into(), Compression::Stored)
 | 
				
			||||||
 | 
					        .size(BATCHED_FILE_SIZE as u64, BATCHED_FILE_SIZE as u64);
 | 
				
			||||||
 | 
					    let mut entry_writer = writer.write_entry_stream(entry).await.unwrap();
 | 
				
			||||||
 | 
					    for _ in 0..NUM_BATCHES {
 | 
				
			||||||
 | 
					        entry_writer.write_all(&[0; BATCH_SIZE]).await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    entry_writer.close().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(writer.is_zip64);
 | 
				
			||||||
 | 
					    let cd_entry = writer.cd_entries.last().unwrap();
 | 
				
			||||||
 | 
					    match &cd_entry.entry.extra_fields.last().unwrap() {
 | 
				
			||||||
 | 
					        ExtraField::Zip64ExtendedInformation(zip64) => {
 | 
				
			||||||
 | 
					            assert_eq!(zip64.compressed_size.unwrap(), BATCHED_FILE_SIZE as u64);
 | 
				
			||||||
 | 
					            assert_eq!(zip64.uncompressed_size.unwrap(), BATCHED_FILE_SIZE as u64);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        e => panic!("Expected a Zip64 extended field, got {:?}", e),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    assert_eq!(cd_entry.header.uncompressed_size, NON_ZIP64_MAX_SIZE);
 | 
				
			||||||
 | 
					    assert_eq!(cd_entry.header.compressed_size, NON_ZIP64_MAX_SIZE);
 | 
				
			||||||
 | 
					    writer.close().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let cursor = std::io::Cursor::new(buffer);
 | 
				
			||||||
 | 
					    let mut archive = zip::read::ZipArchive::new(cursor).unwrap();
 | 
				
			||||||
 | 
					    let mut file = archive.by_name("file").unwrap();
 | 
				
			||||||
 | 
					    assert_eq!(file.compression(), zip::CompressionMethod::Stored);
 | 
				
			||||||
 | 
					    assert_eq!(file.size(), BATCHED_FILE_SIZE as u64);
 | 
				
			||||||
 | 
					    let mut buffer = [0; 100_000];
 | 
				
			||||||
 | 
					    let mut bytes_total = 0;
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					        let read_bytes = file.read(&mut buffer).unwrap();
 | 
				
			||||||
 | 
					        if read_bytes == 0 {
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        bytes_total += read_bytes;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    assert_eq!(bytes_total, BATCHED_FILE_SIZE);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test writing a file, and reading it with async-zip
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_write_large_zip64_file_self_read() {
 | 
				
			||||||
 | 
					    use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init_logger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Allocate space with some extra for metadata records
 | 
				
			||||||
 | 
					    let mut buffer = Vec::with_capacity(BATCHED_FILE_SIZE + 100_000);
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let entry = ZipEntryBuilder::new("file".into(), Compression::Stored);
 | 
				
			||||||
 | 
					    let mut entry_writer = writer.write_entry_stream(entry).await.unwrap();
 | 
				
			||||||
 | 
					    for _ in 0..NUM_BATCHES {
 | 
				
			||||||
 | 
					        entry_writer.write_all(&[0; BATCH_SIZE]).await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    entry_writer.close().await.unwrap();
 | 
				
			||||||
 | 
					    writer.close().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let reader = crate::base::read::mem::ZipFileReader::new(buffer).await.unwrap();
 | 
				
			||||||
 | 
					    assert!(reader.file().zip64);
 | 
				
			||||||
 | 
					    assert_eq!(reader.file().entries[0].entry.filename().as_str().unwrap(), "file");
 | 
				
			||||||
 | 
					    assert_eq!(reader.file().entries[0].entry.compressed_size, BATCHED_FILE_SIZE as u64);
 | 
				
			||||||
 | 
					    let mut entry = reader.reader_without_entry(0).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut buffer = [0; 100_000];
 | 
				
			||||||
 | 
					    let mut bytes_total = 0;
 | 
				
			||||||
 | 
					    loop {
 | 
				
			||||||
 | 
					        let read_bytes = entry.read(&mut buffer).await.unwrap();
 | 
				
			||||||
 | 
					        if read_bytes == 0 {
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        bytes_total += read_bytes;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    assert_eq!(bytes_total, BATCHED_FILE_SIZE);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test writing a zip64 file with more than u16::MAX files.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_write_zip64_file_many_entries() {
 | 
				
			||||||
 | 
					    init_logger();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // The generated file will likely be ~3MB in size.
 | 
				
			||||||
 | 
					    let mut buffer = Vec::with_capacity(3_500_000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut buffer);
 | 
				
			||||||
 | 
					    for i in 0..=u16::MAX as u32 + 1 {
 | 
				
			||||||
 | 
					        let entry = ZipEntryBuilder::new(i.to_string().into(), Compression::Stored);
 | 
				
			||||||
 | 
					        writer.write_entry_whole(entry, &[]).await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    assert!(writer.is_zip64);
 | 
				
			||||||
 | 
					    writer.close().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let cursor = std::io::Cursor::new(buffer);
 | 
				
			||||||
 | 
					    let mut zip = zip::read::ZipArchive::new(cursor).unwrap();
 | 
				
			||||||
 | 
					    assert_eq!(zip.len(), u16::MAX as usize + 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for i in 0..=u16::MAX as u32 + 1 {
 | 
				
			||||||
 | 
					        let mut file = zip.by_name(&i.to_string()).unwrap();
 | 
				
			||||||
 | 
					        let mut buf = Vec::new();
 | 
				
			||||||
 | 
					        file.read_to_end(&mut buf).unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Tests that EntryWholeWriter switches to Zip64 mode when writing too many files for a non-Zip64.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_zip64_when_many_files_whole() {
 | 
				
			||||||
 | 
					    let mut sink = AsyncSink;
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut sink);
 | 
				
			||||||
 | 
					    for i in 0..=u16::MAX as u32 + 1 {
 | 
				
			||||||
 | 
					        let entry = ZipEntryBuilder::new(format!("{i}").into(), Compression::Stored);
 | 
				
			||||||
 | 
					        writer.write_entry_whole(entry, &[]).await.unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    assert!(writer.is_zip64);
 | 
				
			||||||
 | 
					    writer.close().await.unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Tests that EntryStreamWriter switches to Zip64 mode when writing too many files for a non-Zip64.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_zip64_when_many_files_stream() {
 | 
				
			||||||
 | 
					    let mut sink = AsyncSink;
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut sink);
 | 
				
			||||||
 | 
					    for i in 0..=u16::MAX as u32 + 1 {
 | 
				
			||||||
 | 
					        let entry = ZipEntryBuilder::new(format!("{i}").into(), Compression::Stored);
 | 
				
			||||||
 | 
					        let entrywriter = writer.write_entry_stream(entry).await.unwrap();
 | 
				
			||||||
 | 
					        entrywriter.close().await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(writer.is_zip64);
 | 
				
			||||||
 | 
					    writer.close().await.unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Tests that when force_no_zip64 is true, EntryWholeWriter errors when trying to write more than
 | 
				
			||||||
 | 
					/// u16::MAX files to a single archive.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_force_no_zip64_errors_with_too_many_files_whole() {
 | 
				
			||||||
 | 
					    let mut sink = AsyncSink;
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut sink).force_no_zip64();
 | 
				
			||||||
 | 
					    for i in 0..u16::MAX {
 | 
				
			||||||
 | 
					        let entry = ZipEntryBuilder::new(format!("{i}").into(), Compression::Stored);
 | 
				
			||||||
 | 
					        writer.write_entry_whole(entry, &[]).await.unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let entry = ZipEntryBuilder::new("65537".to_string().into(), Compression::Stored);
 | 
				
			||||||
 | 
					    let result = writer.write_entry_whole(entry, &[]).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(matches!(result, Err(ZipError::Zip64Needed(Zip64ErrorCase::TooManyFiles))));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Tests that when force_no_zip64 is true, EntryStreamWriter errors when trying to write more than
 | 
				
			||||||
 | 
					/// u16::MAX files to a single archive.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_force_no_zip64_errors_with_too_many_files_stream() {
 | 
				
			||||||
 | 
					    let mut sink = AsyncSink;
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut sink).force_no_zip64();
 | 
				
			||||||
 | 
					    for i in 0..u16::MAX {
 | 
				
			||||||
 | 
					        let entry = ZipEntryBuilder::new(format!("{i}").into(), Compression::Stored);
 | 
				
			||||||
 | 
					        let entrywriter = writer.write_entry_stream(entry).await.unwrap();
 | 
				
			||||||
 | 
					        entrywriter.close().await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let entry = ZipEntryBuilder::new("65537".to_string().into(), Compression::Stored);
 | 
				
			||||||
 | 
					    let entrywriter = writer.write_entry_stream(entry).await.unwrap();
 | 
				
			||||||
 | 
					    let result = entrywriter.close().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(matches!(result, Err(ZipError::Zip64Needed(Zip64ErrorCase::TooManyFiles))));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Tests that when force_no_zip64 is true, EntryStreamWriter errors when trying to write
 | 
				
			||||||
 | 
					/// a file larger than ~4 GiB to an archive.
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn test_force_no_zip64_errors_with_too_large_file_stream() {
 | 
				
			||||||
 | 
					    let mut sink = AsyncSink;
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut sink).force_no_zip64();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let entry = ZipEntryBuilder::new("-".to_string().into(), Compression::Stored);
 | 
				
			||||||
 | 
					    let mut entrywriter = writer.write_entry_stream(entry).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Writing 4GB, 1kb at a time
 | 
				
			||||||
 | 
					    for _ in 0..NUM_BATCHES {
 | 
				
			||||||
 | 
					        entrywriter.write_all(&[0; BATCH_SIZE]).await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let result = entrywriter.close().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert!(matches!(result, Err(ZipError::Zip64Needed(Zip64ErrorCase::LargeFile))));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								crates/async_zip/src/tokio/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								crates/async_zip/src/tokio/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A set of [`tokio`]-specific type aliases and features.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! # Usage
 | 
				
			||||||
 | 
					//! With the `tokio` feature enabled, types from the [`base`] implementation will implement additional constructors
 | 
				
			||||||
 | 
					//! for use with [`tokio`]. These constructors internally implement conversion between the required async IO traits.
 | 
				
			||||||
 | 
					//! They are defined as:
 | 
				
			||||||
 | 
					//! - [`base::read::seek::ZipFileReader::with_tokio()`]
 | 
				
			||||||
 | 
					//! - [`base::read::stream::ZipFileReader::with_tokio()`]
 | 
				
			||||||
 | 
					//! - [`base::write::ZipFileWriter::with_tokio()`]
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! As a result of Rust's type inference, we are able to reuse the [`base`] implementation's types with considerable
 | 
				
			||||||
 | 
					//! ease. There only exists one caveat with their use; the types returned by these constructors contain a wrapping
 | 
				
			||||||
 | 
					//! compatibility type provided by an external crate. These compatibility types cannot be named unless you also pull in
 | 
				
			||||||
 | 
					//! the [`tokio_util`] dependency manually. This is why we've provided type aliases within this module so that they can
 | 
				
			||||||
 | 
					//! be named without needing to pull in a separate dependency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use crate::base;
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use tokio;
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use tokio_util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod read;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod write {
 | 
				
			||||||
 | 
					    //! A module which supports writing ZIP files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[cfg(doc)]
 | 
				
			||||||
 | 
					    use crate::base;
 | 
				
			||||||
 | 
					    use tokio_util::compat::Compat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// A [`tokio`]-specific type alias for [`base::write::ZipFileWriter`];
 | 
				
			||||||
 | 
					    pub type ZipFileWriter<W> = crate::base::write::ZipFileWriter<Compat<W>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// A [`tokio`]-specific type alias for [`base::write::EntryStreamWriter`];
 | 
				
			||||||
 | 
					    pub type EntryStreamWriter<'a, W> = crate::base::write::EntryStreamWriter<'a, Compat<W>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										160
									
								
								crates/async_zip/src/tokio/read/fs.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								crates/async_zip/src/tokio/read/fs.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,160 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2022 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A concurrent ZIP reader which acts over a file system path.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Concurrency is achieved as a result of:
 | 
				
			||||||
 | 
					//! - Wrapping the provided path within an [`Arc`] to allow shared ownership.
 | 
				
			||||||
 | 
					//! - Constructing a new [`File`] from the path when reading.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Usage
 | 
				
			||||||
 | 
					//! Unlike the [`seek`] module, we no longer hold a mutable reference to any inner reader which in turn, allows the
 | 
				
			||||||
 | 
					//! construction of concurrent [`ZipEntryReader`]s. Though, note that each individual [`ZipEntryReader`] cannot be sent
 | 
				
			||||||
 | 
					//! between thread boundaries due to the masked lifetime requirement. Therefore, the overarching [`ZipFileReader`]
 | 
				
			||||||
 | 
					//! should be cloned and moved into those contexts when needed.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Concurrent Example
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # use async_zip::tokio::read::fs::ZipFileReader;
 | 
				
			||||||
 | 
					//! # use async_zip::error::Result;
 | 
				
			||||||
 | 
					//! # use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! async fn run() -> Result<()> {
 | 
				
			||||||
 | 
					//!     let reader = ZipFileReader::new("./foo.zip").await?;
 | 
				
			||||||
 | 
					//!     let result = tokio::join!(read(&reader, 0), read(&reader, 1));
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     let data_0 = result.0?;
 | 
				
			||||||
 | 
					//!     let data_1 = result.1?;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     // Use data within current scope.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     Ok(())
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! async fn read(reader: &ZipFileReader, index: usize) -> Result<Vec<u8>> {
 | 
				
			||||||
 | 
					//!     let mut entry = reader.reader_without_entry(index).await?;
 | 
				
			||||||
 | 
					//!     let mut data = Vec::new();
 | 
				
			||||||
 | 
					//!     entry.read_to_end(&mut data).await?;
 | 
				
			||||||
 | 
					//!     Ok(data)
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ### Parallel Example
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # use async_zip::tokio::read::fs::ZipFileReader;
 | 
				
			||||||
 | 
					//! # use async_zip::error::Result;
 | 
				
			||||||
 | 
					//! # use futures_lite::io::AsyncReadExt;
 | 
				
			||||||
 | 
					//! #
 | 
				
			||||||
 | 
					//! async fn run() -> Result<()> {
 | 
				
			||||||
 | 
					//!     let reader = ZipFileReader::new("./foo.zip").await?;
 | 
				
			||||||
 | 
					//!     
 | 
				
			||||||
 | 
					//!     let handle_0 = tokio::spawn(read(reader.clone(), 0));
 | 
				
			||||||
 | 
					//!     let handle_1 = tokio::spawn(read(reader.clone(), 1));
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     let data_0 = handle_0.await.expect("thread panicked")?;
 | 
				
			||||||
 | 
					//!     let data_1 = handle_1.await.expect("thread panicked")?;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     // Use data within current scope.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//!     Ok(())
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! async fn read(reader: ZipFileReader, index: usize) -> Result<Vec<u8>> {
 | 
				
			||||||
 | 
					//!     let mut entry = reader.reader_without_entry(index).await?;
 | 
				
			||||||
 | 
					//!     let mut data = Vec::new();
 | 
				
			||||||
 | 
					//!     entry.read_to_end(&mut data).await?;
 | 
				
			||||||
 | 
					//!     Ok(data)
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use crate::base::read::seek;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::base::read::io::entry::{WithEntry, WithoutEntry, ZipEntryReader};
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use crate::file::ZipFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::path::{Path, PathBuf};
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use tokio::fs::File;
 | 
				
			||||||
 | 
					use tokio::io::BufReader;
 | 
				
			||||||
 | 
					use tokio_util::compat::{Compat, TokioAsyncReadCompatExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Inner {
 | 
				
			||||||
 | 
					    path: PathBuf,
 | 
				
			||||||
 | 
					    file: ZipFile,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A concurrent ZIP reader which acts over a file system path.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct ZipFileReader {
 | 
				
			||||||
 | 
					    inner: Arc<Inner>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ZipFileReader {
 | 
				
			||||||
 | 
					    /// Constructs a new ZIP reader from a file system path.
 | 
				
			||||||
 | 
					    pub async fn new<P>(path: P) -> Result<ZipFileReader>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        P: AsRef<Path>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let file = crate::base::read::file(File::open(&path).await?.compat()).await?;
 | 
				
			||||||
 | 
					        Ok(ZipFileReader::from_raw_parts(path, file))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Constructs a ZIP reader from a file system path and ZIP file information derived from that path.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Providing a [`ZipFile`] that wasn't derived from that path may lead to inaccurate parsing.
 | 
				
			||||||
 | 
					    pub fn from_raw_parts<P>(path: P, file: ZipFile) -> ZipFileReader
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        P: AsRef<Path>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        ZipFileReader { inner: Arc::new(Inner { path: path.as_ref().to_owned(), file }) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns this ZIP file's information.
 | 
				
			||||||
 | 
					    pub fn file(&self) -> &ZipFile {
 | 
				
			||||||
 | 
					        &self.inner.file
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the file system path provided to the reader during construction.
 | 
				
			||||||
 | 
					    pub fn path(&self) -> &Path {
 | 
				
			||||||
 | 
					        &self.inner.path
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a new entry reader if the provided index is valid.
 | 
				
			||||||
 | 
					    pub async fn reader_without_entry(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        index: usize,
 | 
				
			||||||
 | 
					    ) -> Result<ZipEntryReader<'static, Compat<BufReader<File>>, WithoutEntry>> {
 | 
				
			||||||
 | 
					        let stored_entry = self.inner.file.entries.get(index).ok_or(ZipError::EntryIndexOutOfBounds)?;
 | 
				
			||||||
 | 
					        let mut fs_file = BufReader::new(File::open(&self.inner.path).await?).compat();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stored_entry.seek_to_data_offset(&mut fs_file).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(ZipEntryReader::new_with_owned(
 | 
				
			||||||
 | 
					            fs_file,
 | 
				
			||||||
 | 
					            stored_entry.entry.compression(),
 | 
				
			||||||
 | 
					            stored_entry.entry.compressed_size(),
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns a new entry reader if the provided index is valid.
 | 
				
			||||||
 | 
					    pub async fn reader_with_entry(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        index: usize,
 | 
				
			||||||
 | 
					    ) -> Result<ZipEntryReader<'_, Compat<BufReader<File>>, WithEntry<'_>>> {
 | 
				
			||||||
 | 
					        let stored_entry = self.inner.file.entries.get(index).ok_or(ZipError::EntryIndexOutOfBounds)?;
 | 
				
			||||||
 | 
					        let mut fs_file = BufReader::new(File::open(&self.inner.path).await?).compat();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stored_entry.seek_to_data_offset(&mut fs_file).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let reader = ZipEntryReader::new_with_owned(
 | 
				
			||||||
 | 
					            fs_file,
 | 
				
			||||||
 | 
					            stored_entry.entry.compression(),
 | 
				
			||||||
 | 
					            stored_entry.entry.compressed_size(),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(reader.into_with_entry(stored_entry))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										44
									
								
								crates/async_zip/src/tokio/read/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								crates/async_zip/src/tokio/read/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//! A module which supports reading ZIP files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use tokio_util::compat::Compat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio-fs")]
 | 
				
			||||||
 | 
					pub mod fs;
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use crate::base;
 | 
				
			||||||
 | 
					#[cfg(doc)]
 | 
				
			||||||
 | 
					use tokio;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A [`tokio`]-specific type alias for [`base::read::ZipEntryReader`];
 | 
				
			||||||
 | 
					pub type ZipEntryReader<'a, R, E> = crate::base::read::ZipEntryReader<'a, Compat<R>, E>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod seek {
 | 
				
			||||||
 | 
					    //! A ZIP reader which acts over a seekable source.
 | 
				
			||||||
 | 
					    use tokio_util::compat::Compat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[cfg(doc)]
 | 
				
			||||||
 | 
					    use crate::base;
 | 
				
			||||||
 | 
					    #[cfg(doc)]
 | 
				
			||||||
 | 
					    use tokio;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// A [`tokio`]-specific type alias for [`base::read::seek::ZipFileReader`];
 | 
				
			||||||
 | 
					    pub type ZipFileReader<R> = crate::base::read::seek::ZipFileReader<Compat<R>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod stream {
 | 
				
			||||||
 | 
					    //! A ZIP reader which acts over a non-seekable source.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[cfg(doc)]
 | 
				
			||||||
 | 
					    use crate::base;
 | 
				
			||||||
 | 
					    #[cfg(doc)]
 | 
				
			||||||
 | 
					    use tokio;
 | 
				
			||||||
 | 
					    use tokio_util::compat::Compat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// A [`tokio`]-specific type alias for [`base::read::stream::Reading`];
 | 
				
			||||||
 | 
					    pub type Reading<'a, R, E> = crate::base::read::stream::Reading<'a, Compat<R>, E>;
 | 
				
			||||||
 | 
					    /// A [`tokio`]-specific type alias for [`base::read::stream::Ready`];
 | 
				
			||||||
 | 
					    pub type Ready<R> = crate::base::read::stream::Ready<Compat<R>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								crates/async_zip/src/utils.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								crates/async_zip/src/utils.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::error::{Result, ZipError};
 | 
				
			||||||
 | 
					use futures_lite::io::{AsyncRead, AsyncReadExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Assert that the next four-byte signature read by a reader which impls AsyncRead matches the expected signature.
 | 
				
			||||||
 | 
					pub(crate) async fn assert_signature<R: AsyncRead + Unpin>(reader: &mut R, expected: u32) -> Result<()> {
 | 
				
			||||||
 | 
					    let signature = {
 | 
				
			||||||
 | 
					        let mut buffer = [0; 4];
 | 
				
			||||||
 | 
					        reader.read_exact(&mut buffer).await?;
 | 
				
			||||||
 | 
					        u32::from_le_bytes(buffer)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    match signature {
 | 
				
			||||||
 | 
					        actual if actual == expected => Ok(()),
 | 
				
			||||||
 | 
					        actual => Err(ZipError::UnexpectedHeaderError(actual, expected)),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										99
									
								
								crates/async_zip/tests/common/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								crates/async_zip/tests/common/mod.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,99 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					#![allow(dead_code)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use async_zip::base::read::mem;
 | 
				
			||||||
 | 
					use async_zip::base::read::seek;
 | 
				
			||||||
 | 
					use async_zip::base::write::ZipFileWriter;
 | 
				
			||||||
 | 
					use async_zip::Compression;
 | 
				
			||||||
 | 
					use async_zip::ZipEntryBuilder;
 | 
				
			||||||
 | 
					use futures_lite::io::AsyncWriteExt;
 | 
				
			||||||
 | 
					use tokio::fs::File;
 | 
				
			||||||
 | 
					use tokio::io::BufReader;
 | 
				
			||||||
 | 
					use tokio_util::compat::TokioAsyncReadCompatExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FOLDER_PREFIX: &str = "tests/test_inputs";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FILE_LIST: &[&str] = &[
 | 
				
			||||||
 | 
					    "sample_data/alpha/back_to_front.txt",
 | 
				
			||||||
 | 
					    "sample_data/alpha/front_to_back.txt",
 | 
				
			||||||
 | 
					    "sample_data/numeric/forward.txt",
 | 
				
			||||||
 | 
					    "sample_data/numeric/reverse.txt",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn compress_to_mem(compress: Compression) -> Vec<u8> {
 | 
				
			||||||
 | 
					    let mut bytes = Vec::with_capacity(10_000);
 | 
				
			||||||
 | 
					    let mut writer = ZipFileWriter::new(&mut bytes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for fname in FILE_LIST {
 | 
				
			||||||
 | 
					        let content = tokio::fs::read(format!("{FOLDER_PREFIX}/{fname}")).await.unwrap();
 | 
				
			||||||
 | 
					        let opts = ZipEntryBuilder::new(fname.to_string().into(), compress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut entry_writer = writer.write_entry_stream(opts).await.unwrap();
 | 
				
			||||||
 | 
					        entry_writer.write_all(&content).await.unwrap();
 | 
				
			||||||
 | 
					        entry_writer.close().await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    writer.close().await.unwrap();
 | 
				
			||||||
 | 
					    bytes
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio-fs")]
 | 
				
			||||||
 | 
					pub async fn check_decompress_fs(fname: &str) {
 | 
				
			||||||
 | 
					    use async_zip::tokio::read::fs;
 | 
				
			||||||
 | 
					    let zip = fs::ZipFileReader::new(fname).await.unwrap();
 | 
				
			||||||
 | 
					    let zip_entries: Vec<_> = zip.file().entries().to_vec();
 | 
				
			||||||
 | 
					    for (idx, entry) in zip_entries.into_iter().enumerate() {
 | 
				
			||||||
 | 
					        // TODO: resolve unwrap usage
 | 
				
			||||||
 | 
					        if entry.dir().unwrap() {
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // TODO: resolve unwrap usage
 | 
				
			||||||
 | 
					        let fname = entry.filename().as_str().unwrap();
 | 
				
			||||||
 | 
					        let mut output = String::new();
 | 
				
			||||||
 | 
					        let mut reader = zip.reader_with_entry(idx).await.unwrap();
 | 
				
			||||||
 | 
					        let _ = reader.read_to_string_checked(&mut output).await.unwrap();
 | 
				
			||||||
 | 
					        let fs_file = format!("{FOLDER_PREFIX}/{fname}");
 | 
				
			||||||
 | 
					        let expected = tokio::fs::read_to_string(fs_file).await.unwrap();
 | 
				
			||||||
 | 
					        assert_eq!(output, expected, "for {fname}, expect zip data to match file data");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn check_decompress_seek(fname: &str) {
 | 
				
			||||||
 | 
					    let file = BufReader::new(File::open(fname).await.unwrap());
 | 
				
			||||||
 | 
					    let mut file_compat = file.compat();
 | 
				
			||||||
 | 
					    let mut zip = seek::ZipFileReader::new(&mut file_compat).await.unwrap();
 | 
				
			||||||
 | 
					    let zip_entries: Vec<_> = zip.file().entries().to_vec();
 | 
				
			||||||
 | 
					    for (idx, entry) in zip_entries.into_iter().enumerate() {
 | 
				
			||||||
 | 
					        // TODO: resolve unwrap usage
 | 
				
			||||||
 | 
					        if entry.dir().unwrap() {
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // TODO: resolve unwrap usage
 | 
				
			||||||
 | 
					        let fname = entry.filename().as_str().unwrap();
 | 
				
			||||||
 | 
					        let mut output = String::new();
 | 
				
			||||||
 | 
					        let mut reader = zip.reader_with_entry(idx).await.unwrap();
 | 
				
			||||||
 | 
					        let _ = reader.read_to_string_checked(&mut output).await.unwrap();
 | 
				
			||||||
 | 
					        let fs_file = format!("tests/test_inputs/{fname}");
 | 
				
			||||||
 | 
					        let expected = tokio::fs::read_to_string(fs_file).await.unwrap();
 | 
				
			||||||
 | 
					        assert_eq!(output, expected, "for {fname}, expect zip data to match file data");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn check_decompress_mem(zip_data: Vec<u8>) {
 | 
				
			||||||
 | 
					    let zip = mem::ZipFileReader::new(zip_data).await.unwrap();
 | 
				
			||||||
 | 
					    let zip_entries: Vec<_> = zip.file().entries().to_vec();
 | 
				
			||||||
 | 
					    for (idx, entry) in zip_entries.into_iter().enumerate() {
 | 
				
			||||||
 | 
					        // TODO: resolve unwrap usage
 | 
				
			||||||
 | 
					        if entry.dir().unwrap() {
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // TODO: resolve unwrap usage
 | 
				
			||||||
 | 
					        let fname = entry.filename().as_str().unwrap();
 | 
				
			||||||
 | 
					        let mut output = String::new();
 | 
				
			||||||
 | 
					        let mut reader = zip.reader_with_entry(idx).await.unwrap();
 | 
				
			||||||
 | 
					        let _ = reader.read_to_string_checked(&mut output).await.unwrap();
 | 
				
			||||||
 | 
					        let fs_file = format!("{FOLDER_PREFIX}/{fname}");
 | 
				
			||||||
 | 
					        let expected = tokio::fs::read_to_string(fs_file).await.unwrap();
 | 
				
			||||||
 | 
					        assert_eq!(output, expected, "for {fname}, expect zip data to match file data");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										81
									
								
								crates/async_zip/tests/compress_test.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								crates/async_zip/tests/compress_test.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,81 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use async_zip::{Compression, ZipEntryBuilder, ZipString};
 | 
				
			||||||
 | 
					use futures_lite::AsyncWriteExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod common;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn zip_zstd_in_out() {
 | 
				
			||||||
 | 
					    let zip_data = common::compress_to_mem(Compression::Zstd).await;
 | 
				
			||||||
 | 
					    common::check_decompress_mem(zip_data).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn zip_decompress_in_out() {
 | 
				
			||||||
 | 
					    let zip_data = common::compress_to_mem(Compression::Deflate).await;
 | 
				
			||||||
 | 
					    common::check_decompress_mem(zip_data).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn zip_store_in_out() {
 | 
				
			||||||
 | 
					    let zip_data = common::compress_to_mem(Compression::Stored).await;
 | 
				
			||||||
 | 
					    common::check_decompress_mem(zip_data).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn zip_utf8_extra_in_out_stream() {
 | 
				
			||||||
 | 
					    let mut zip_bytes = Vec::with_capacity(10_000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // writing
 | 
				
			||||||
 | 
					        let content = "Test".as_bytes();
 | 
				
			||||||
 | 
					        let mut writer = async_zip::base::write::ZipFileWriter::new(&mut zip_bytes);
 | 
				
			||||||
 | 
					        let filename =
 | 
				
			||||||
 | 
					            ZipString::new_with_alternative("\u{4E2D}\u{6587}.txt".to_string(), b"\xD6\xD0\xCe\xC4.txt".to_vec());
 | 
				
			||||||
 | 
					        let opts = ZipEntryBuilder::new(filename, Compression::Stored);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut entry_writer = writer.write_entry_stream(opts).await.unwrap();
 | 
				
			||||||
 | 
					        entry_writer.write_all(content).await.unwrap();
 | 
				
			||||||
 | 
					        entry_writer.close().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        writer.close().await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // reading
 | 
				
			||||||
 | 
					        let zip = async_zip::base::read::mem::ZipFileReader::new(zip_bytes).await.unwrap();
 | 
				
			||||||
 | 
					        let zip_entries: Vec<_> = zip.file().entries().to_vec();
 | 
				
			||||||
 | 
					        assert_eq!(zip_entries.len(), 1);
 | 
				
			||||||
 | 
					        assert_eq!(zip_entries[0].filename().as_str().unwrap(), "\u{4E2D}\u{6587}.txt");
 | 
				
			||||||
 | 
					        assert_eq!(zip_entries[0].filename().alternative(), Some(b"\xD6\xD0\xCe\xC4.txt".as_ref()));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn zip_utf8_extra_in_out_whole() {
 | 
				
			||||||
 | 
					    let mut zip_bytes = Vec::with_capacity(10_000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // writing
 | 
				
			||||||
 | 
					        let content = "Test".as_bytes();
 | 
				
			||||||
 | 
					        let mut writer = async_zip::base::write::ZipFileWriter::new(&mut zip_bytes);
 | 
				
			||||||
 | 
					        let filename =
 | 
				
			||||||
 | 
					            ZipString::new_with_alternative("\u{4E2D}\u{6587}.txt".to_string(), b"\xD6\xD0\xCe\xC4.txt".to_vec());
 | 
				
			||||||
 | 
					        let opts = ZipEntryBuilder::new(filename, Compression::Stored);
 | 
				
			||||||
 | 
					        writer.write_entry_whole(opts, content).await.unwrap();
 | 
				
			||||||
 | 
					        writer.close().await.unwrap();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // reading
 | 
				
			||||||
 | 
					        let zip = async_zip::base::read::mem::ZipFileReader::new(zip_bytes).await.unwrap();
 | 
				
			||||||
 | 
					        let zip_entries: Vec<_> = zip.file().entries().to_vec();
 | 
				
			||||||
 | 
					        assert_eq!(zip_entries.len(), 1);
 | 
				
			||||||
 | 
					        assert_eq!(zip_entries[0].filename().as_str().unwrap(), "\u{4E2D}\u{6587}.txt");
 | 
				
			||||||
 | 
					        assert_eq!(zip_entries[0].filename().alternative(), Some(b"\xD6\xD0\xCe\xC4.txt".as_ref()));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								crates/async_zip/tests/decompress_test.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								crates/async_zip/tests/decompress_test.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,89 @@
 | 
				
			||||||
 | 
					// Copyright (c) 2023 Harry [Majored] [hello@majored.pw]
 | 
				
			||||||
 | 
					// MIT License (https://github.com/Majored/rs-async-zip/blob/main/LICENSE)
 | 
				
			||||||
 | 
					#![allow(dead_code)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use tokio::io::BufReader;
 | 
				
			||||||
 | 
					use tokio_util::compat::TokioAsyncReadCompatExt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mod common;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ZSTD_ZIP_FILE: &str = "tests/test_inputs/sample_data.zstd.zip";
 | 
				
			||||||
 | 
					const DEFLATE_ZIP_FILE: &str = "tests/test_inputs/sample_data.deflate.zip";
 | 
				
			||||||
 | 
					const STORE_ZIP_FILE: &str = "tests/test_inputs/sample_data.store.zip";
 | 
				
			||||||
 | 
					const UTF8_EXTRA_ZIP_FILE: &str = "tests/test_inputs/sample_data_utf8_extra.zip";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_zstd_zip_seek() {
 | 
				
			||||||
 | 
					    common::check_decompress_seek(ZSTD_ZIP_FILE).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_deflate_zip_seek() {
 | 
				
			||||||
 | 
					    common::check_decompress_seek(DEFLATE_ZIP_FILE).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn check_empty_zip_seek() {
 | 
				
			||||||
 | 
					    let mut data: Vec<u8> = Vec::new();
 | 
				
			||||||
 | 
					    async_zip::base::write::ZipFileWriter::new(futures_lite::io::Cursor::new(&mut data)).close().await.unwrap();
 | 
				
			||||||
 | 
					    async_zip::base::read::seek::ZipFileReader::new(futures_lite::io::Cursor::new(&data)).await.unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_store_zip_seek() {
 | 
				
			||||||
 | 
					    common::check_decompress_seek(STORE_ZIP_FILE).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_zstd_zip_mem() {
 | 
				
			||||||
 | 
					    let content = tokio::fs::read(ZSTD_ZIP_FILE).await.unwrap();
 | 
				
			||||||
 | 
					    common::check_decompress_mem(content).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_deflate_zip_mem() {
 | 
				
			||||||
 | 
					    let content = tokio::fs::read(DEFLATE_ZIP_FILE).await.unwrap();
 | 
				
			||||||
 | 
					    common::check_decompress_mem(content).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_store_zip_mem() {
 | 
				
			||||||
 | 
					    let content = tokio::fs::read(STORE_ZIP_FILE).await.unwrap();
 | 
				
			||||||
 | 
					    common::check_decompress_mem(content).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "zstd")]
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio-fs")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_zstd_zip_fs() {
 | 
				
			||||||
 | 
					    common::check_decompress_fs(ZSTD_ZIP_FILE).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "deflate")]
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio-fs")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_deflate_zip_fs() {
 | 
				
			||||||
 | 
					    common::check_decompress_fs(DEFLATE_ZIP_FILE).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(feature = "tokio-fs")]
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_store_zip_fs() {
 | 
				
			||||||
 | 
					    common::check_decompress_fs(STORE_ZIP_FILE).await
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::test]
 | 
				
			||||||
 | 
					async fn decompress_zip_with_utf8_extra() {
 | 
				
			||||||
 | 
					    let file = BufReader::new(tokio::fs::File::open(UTF8_EXTRA_ZIP_FILE).await.unwrap());
 | 
				
			||||||
 | 
					    let mut file_compat = file.compat();
 | 
				
			||||||
 | 
					    let zip = async_zip::base::read::seek::ZipFileReader::new(&mut file_compat).await.unwrap();
 | 
				
			||||||
 | 
					    let zip_entries: Vec<_> = zip.file().entries().to_vec();
 | 
				
			||||||
 | 
					    assert_eq!(zip_entries.len(), 1);
 | 
				
			||||||
 | 
					    assert_eq!(zip_entries[0].header_size(), 93);
 | 
				
			||||||
 | 
					    assert_eq!(zip_entries[0].filename().as_str().unwrap(), "\u{4E2D}\u{6587}.txt");
 | 
				
			||||||
 | 
					    assert_eq!(zip_entries[0].filename().alternative(), Some(b"\xD6\xD0\xCe\xC4.txt".as_ref()));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data.deflate.zip
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data.deflate.zip
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data.store.zip
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data.store.zip
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data.zstd.zip
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data.zstd.zip
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					Z,z,Y,y,X,x,W,w,V,v,U,u,T,t,S,s,R,r,Q,q,P,p,O,o,N,n,M,m,L,l,K,k,J,j,I,I,H,h,G,g,F,f,E,e,D,d,C,c,B,b,A,a
 | 
				
			||||||
 | 
					Z,z,Y,y,X,x,W,w,V,v,U,u,T,t,S,s,R,r,Q,q,P,p,O,o,N,n,M,m,L,l,K,k,J,j,I,I,H,h,G,g,F,f,E,e,D,d,C,c,B,b,A,a
 | 
				
			||||||
 | 
					Z,z,Y,y,X,x,W,w,V,v,U,u,T,t,S,s,R,r,Q,q,P,p,O,o,N,n,M,m,L,l,K,k,J,j,I,I,H,h,G,g,F,f,E,e,D,d,C,c,B,b,A,a
 | 
				
			||||||
 | 
					Z,z,Y,y,X,x,W,w,V,v,U,u,T,t,S,s,R,r,Q,q,P,p,O,o,N,n,M,m,L,l,K,k,J,j,I,I,H,h,G,g,F,f,E,e,D,d,C,c,B,b,A,a
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					A,a,B,b,C,c,D,d,E,e,F,f,G,g,H,h,I,I,J,j,K,k,L,l,M,m,N,n,O,o,P,p,Q,q,R,r,S,s,T,t,U,u,V,v,W,w,X,x,Y,y,Z,z
 | 
				
			||||||
 | 
					A,a,B,b,C,c,D,d,E,e,F,f,G,g,H,h,I,I,J,j,K,k,L,l,M,m,N,n,O,o,P,p,Q,q,R,r,S,s,T,t,U,u,V,v,W,w,X,x,Y,y,Z,z
 | 
				
			||||||
 | 
					A,a,B,b,C,c,D,d,E,e,F,f,G,g,H,h,I,I,J,j,K,k,L,l,M,m,N,n,O,o,P,p,Q,q,R,r,S,s,T,t,U,u,V,v,W,w,X,x,Y,y,Z,z
 | 
				
			||||||
 | 
					A,a,B,b,C,c,D,d,E,e,F,f,G,g,H,h,I,I,J,j,K,k,L,l,M,m,N,n,O,o,P,p,Q,q,R,r,S,s,T,t,U,u,V,v,W,w,X,x,Y,y,Z,z
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data_utf8_extra.zip
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								crates/async_zip/tests/test_inputs/sample_data_utf8_extra.zip
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										21
									
								
								crates/envy/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								crates/envy/Cargo.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "envy"
 | 
				
			||||||
 | 
					version = "0.4.2"
 | 
				
			||||||
 | 
					authors = ["softprops <d.tangren@gmail.com>"]
 | 
				
			||||||
 | 
					description = "deserialize env vars into typesafe structs"
 | 
				
			||||||
 | 
					documentation = "https://softprops.github.io/envy"
 | 
				
			||||||
 | 
					homepage = "https://github.com/softprops/envy"
 | 
				
			||||||
 | 
					repository = "https://github.com/softprops/envy"
 | 
				
			||||||
 | 
					keywords = ["serde", "env"]
 | 
				
			||||||
 | 
					license = "MIT"
 | 
				
			||||||
 | 
					readme = "README.md"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					categories = [
 | 
				
			||||||
 | 
					  "config"
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					serde = "1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dev-dependencies]
 | 
				
			||||||
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
							
								
								
									
										55
									
								
								crates/envy/src/error.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								crates/envy/src/error.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					//! Error types
 | 
				
			||||||
 | 
					use serde::de::Error as SerdeError;
 | 
				
			||||||
 | 
					use std::{error::Error as StdError, fmt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Types of errors that may result from failed attempts
 | 
				
			||||||
 | 
					/// to deserialize a type from env vars
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq)]
 | 
				
			||||||
 | 
					pub enum Error {
 | 
				
			||||||
 | 
					    MissingValue(String),
 | 
				
			||||||
 | 
					    Custom(String),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl StdError for Error {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl fmt::Display for Error {
 | 
				
			||||||
 | 
					    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Error::MissingValue(field) => write!(fmt, "missing value for {}", &field),
 | 
				
			||||||
 | 
					            Error::Custom(ref msg) => write!(fmt, "{}", msg),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl SerdeError for Error {
 | 
				
			||||||
 | 
					    fn custom<T: fmt::Display>(msg: T) -> Self {
 | 
				
			||||||
 | 
					        Error::Custom(format!("{}", msg))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn missing_field(field: &'static str) -> Error {
 | 
				
			||||||
 | 
					        Error::MissingValue(field.into())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn impl_std_error<E: StdError>(_: E) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn error_impl_std_error() {
 | 
				
			||||||
 | 
					        impl_std_error(Error::MissingValue("FOO_BAR".into()));
 | 
				
			||||||
 | 
					        impl_std_error(Error::Custom("whoops".into()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn error_display() {
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            format!("{}", Error::MissingValue("FOO_BAR".into())),
 | 
				
			||||||
 | 
					            "missing value for FOO_BAR"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(format!("{}", Error::Custom("whoops".into())), "whoops")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										560
									
								
								crates/envy/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										560
									
								
								crates/envy/src/lib.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,560 @@
 | 
				
			||||||
 | 
					//! Envy is a library for deserializing environment variables into typesafe structs
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! # Examples
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! A typical usecase for envy is deserializing configuration store in an process' environment into a struct
 | 
				
			||||||
 | 
					//! whose fields map to the names of env vars.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Serde makes it easy to provide a deserializable struct with its [deriveable Deserialize](https://serde.rs/derive.html)
 | 
				
			||||||
 | 
					//! procedural macro.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Simply ask for an instance of that struct from envy's `from_env` function.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! use serde::Deserialize;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! #[derive(Deserialize, Debug)]
 | 
				
			||||||
 | 
					//! struct Config {
 | 
				
			||||||
 | 
					//!     foo: u16,
 | 
				
			||||||
 | 
					//!     bar: bool,
 | 
				
			||||||
 | 
					//!     baz: String,
 | 
				
			||||||
 | 
					//!     boom: Option<u64>,
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! match envy::from_env::<Config>() {
 | 
				
			||||||
 | 
					//!     Ok(config) => println!("{:#?}", config),
 | 
				
			||||||
 | 
					//!     Err(error) => eprintln!("{:#?}", error),
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Special treatment is given to collections. For config fields that store a `Vec` of values,
 | 
				
			||||||
 | 
					//! use an env var that uses a comma separated value.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! All serde modifiers should work as is.
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! Enums with unit variants can be used as values:
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! ```no_run
 | 
				
			||||||
 | 
					//! # use serde::Deserialize;
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! #[derive(Deserialize, Debug, PartialEq)]
 | 
				
			||||||
 | 
					//! #[serde(rename_all = "lowercase")]
 | 
				
			||||||
 | 
					//! pub enum Size {
 | 
				
			||||||
 | 
					//!     Small,
 | 
				
			||||||
 | 
					//!     Medium,
 | 
				
			||||||
 | 
					//!     Large,
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! #[derive(Deserialize, Debug)]
 | 
				
			||||||
 | 
					//! struct Config {
 | 
				
			||||||
 | 
					//!     size: Size,
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//!
 | 
				
			||||||
 | 
					//! // set env var for size as `SIZE=medium`
 | 
				
			||||||
 | 
					//! match envy::from_env::<Config>() {
 | 
				
			||||||
 | 
					//!     Ok(config) => println!("{:#?}", config),
 | 
				
			||||||
 | 
					//!     Err(error) => eprintln!("{:#?}", error),
 | 
				
			||||||
 | 
					//! }
 | 
				
			||||||
 | 
					//! ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use serde::de::{
 | 
				
			||||||
 | 
					    self,
 | 
				
			||||||
 | 
					    value::{MapDeserializer, SeqDeserializer},
 | 
				
			||||||
 | 
					    IntoDeserializer,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    borrow::Cow,
 | 
				
			||||||
 | 
					    env,
 | 
				
			||||||
 | 
					    iter::{empty, IntoIterator},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Ours
 | 
				
			||||||
 | 
					mod error;
 | 
				
			||||||
 | 
					pub use crate::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type result type specific to `envy::Errors`
 | 
				
			||||||
 | 
					pub type Result<T> = std::result::Result<T, Error>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Vars<Iter>(Iter)
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    Iter: IntoIterator<Item = (String, String)>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Val(String, String);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'de> IntoDeserializer<'de, Error> for Val {
 | 
				
			||||||
 | 
					    type Deserializer = Self;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn into_deserializer(self) -> Self::Deserializer {
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct VarName(String);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'de> IntoDeserializer<'de, Error> for VarName {
 | 
				
			||||||
 | 
					    type Deserializer = Self;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn into_deserializer(self) -> Self::Deserializer {
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<Iter: Iterator<Item = (String, String)>> Iterator for Vars<Iter> {
 | 
				
			||||||
 | 
					    type Item = (VarName, Val);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn next(&mut self) -> Option<Self::Item> {
 | 
				
			||||||
 | 
					        self.0
 | 
				
			||||||
 | 
					            .next()
 | 
				
			||||||
 | 
					            .map(|(k, v)| (VarName(k.to_lowercase()), Val(k, v)))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! forward_parsed_values {
 | 
				
			||||||
 | 
					    ($($ty:ident => $method:ident,)*) => {
 | 
				
			||||||
 | 
					        $(
 | 
				
			||||||
 | 
					            fn $method<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					                where V: de::Visitor<'de>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                match self.1.parse::<$ty>() {
 | 
				
			||||||
 | 
					                    Ok(val) => val.into_deserializer().$method(visitor),
 | 
				
			||||||
 | 
					                    Err(e) => Err(de::Error::custom(format_args!("{} while parsing value '{}' provided by {}", e, self.1, self.0)))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )*
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'de> de::Deserializer<'de> for Val {
 | 
				
			||||||
 | 
					    type Error = Error;
 | 
				
			||||||
 | 
					    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        self.1.into_deserializer().deserialize_any(visitor)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // std::str::split doesn't work as expected for our use case: when we
 | 
				
			||||||
 | 
					        // get an empty string we want to produce an empty Vec, but split would
 | 
				
			||||||
 | 
					        // still yield an iterator with an empty string in it. So we need to
 | 
				
			||||||
 | 
					        // special case empty strings.
 | 
				
			||||||
 | 
					        if self.1.is_empty() {
 | 
				
			||||||
 | 
					            SeqDeserializer::new(empty::<Val>()).deserialize_seq(visitor)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let values = self
 | 
				
			||||||
 | 
					                .1
 | 
				
			||||||
 | 
					                .split(',')
 | 
				
			||||||
 | 
					                .map(|v| Val(self.0.clone(), v.trim().to_owned()));
 | 
				
			||||||
 | 
					            SeqDeserializer::new(values).deserialize_seq(visitor)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if self.1.is_empty() {
 | 
				
			||||||
 | 
					            visitor.visit_none()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            visitor.visit_some(self)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    forward_parsed_values! {
 | 
				
			||||||
 | 
					        u8 => deserialize_u8,
 | 
				
			||||||
 | 
					        u16 => deserialize_u16,
 | 
				
			||||||
 | 
					        u32 => deserialize_u32,
 | 
				
			||||||
 | 
					        u64 => deserialize_u64,
 | 
				
			||||||
 | 
					        u128 => deserialize_u128,
 | 
				
			||||||
 | 
					        i8 => deserialize_i8,
 | 
				
			||||||
 | 
					        i16 => deserialize_i16,
 | 
				
			||||||
 | 
					        i32 => deserialize_i32,
 | 
				
			||||||
 | 
					        i64 => deserialize_i64,
 | 
				
			||||||
 | 
					        i128 => deserialize_i128,
 | 
				
			||||||
 | 
					        f32 => deserialize_f32,
 | 
				
			||||||
 | 
					        f64 => deserialize_f64,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if self.1 == "1" || self.1.eq_ignore_ascii_case("true") {
 | 
				
			||||||
 | 
					            visitor.visit_bool(true)
 | 
				
			||||||
 | 
					        } else if self.1 == "0" || self.0.eq_ignore_ascii_case("false") {
 | 
				
			||||||
 | 
					            visitor.visit_bool(false)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(de::Error::custom(format_args!(
 | 
				
			||||||
 | 
					                "error parsing boolean value: '{}'",
 | 
				
			||||||
 | 
					                self.1
 | 
				
			||||||
 | 
					            )))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[inline]
 | 
				
			||||||
 | 
					    fn deserialize_newtype_struct<V>(self, _: &'static str, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: serde::de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        visitor.visit_newtype_struct(self)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn deserialize_enum<V>(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        _name: &'static str,
 | 
				
			||||||
 | 
					        _variants: &'static [&'static str],
 | 
				
			||||||
 | 
					        visitor: V,
 | 
				
			||||||
 | 
					    ) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        visitor.visit_enum(self.1.into_deserializer())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    serde::forward_to_deserialize_any! {
 | 
				
			||||||
 | 
					        char str string unit
 | 
				
			||||||
 | 
					        bytes byte_buf map unit_struct tuple_struct
 | 
				
			||||||
 | 
					        identifier tuple ignored_any
 | 
				
			||||||
 | 
					        struct
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'de> de::Deserializer<'de> for VarName {
 | 
				
			||||||
 | 
					    type Error = Error;
 | 
				
			||||||
 | 
					    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        self.0.into_deserializer().deserialize_any(visitor)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[inline]
 | 
				
			||||||
 | 
					    fn deserialize_newtype_struct<V>(self, _: &'static str, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: serde::de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        visitor.visit_newtype_struct(self)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    serde::forward_to_deserialize_any! {
 | 
				
			||||||
 | 
					        char str string unit seq option
 | 
				
			||||||
 | 
					        bytes byte_buf map unit_struct tuple_struct
 | 
				
			||||||
 | 
					        identifier tuple ignored_any enum
 | 
				
			||||||
 | 
					        struct bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A deserializer for env vars
 | 
				
			||||||
 | 
					struct Deserializer<'de, Iter: Iterator<Item = (String, String)>> {
 | 
				
			||||||
 | 
					    inner: MapDeserializer<'de, Vars<Iter>, Error>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'de, Iter: Iterator<Item = (String, String)>> Deserializer<'de, Iter> {
 | 
				
			||||||
 | 
					    fn new(vars: Iter) -> Self {
 | 
				
			||||||
 | 
					        Deserializer {
 | 
				
			||||||
 | 
					            inner: MapDeserializer::new(Vars(vars)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'de, Iter: Iterator<Item = (String, String)>> de::Deserializer<'de>
 | 
				
			||||||
 | 
					    for Deserializer<'de, Iter>
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    type Error = Error;
 | 
				
			||||||
 | 
					    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        self.deserialize_map(visitor)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        V: de::Visitor<'de>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        visitor.visit_map(self.inner)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    serde::forward_to_deserialize_any! {
 | 
				
			||||||
 | 
					        bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq
 | 
				
			||||||
 | 
					        bytes byte_buf unit_struct tuple_struct
 | 
				
			||||||
 | 
					        identifier tuple ignored_any option newtype_struct enum
 | 
				
			||||||
 | 
					        struct
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Deserializes a type based on information stored in env variables
 | 
				
			||||||
 | 
					pub fn from_env<T>() -> Result<T>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    T: de::DeserializeOwned,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    from_iter(env::vars())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Deserializes a type based on an iterable of `(String, String)`
 | 
				
			||||||
 | 
					/// representing keys and values
 | 
				
			||||||
 | 
					pub fn from_iter<Iter, T>(iter: Iter) -> Result<T>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    T: de::DeserializeOwned,
 | 
				
			||||||
 | 
					    Iter: IntoIterator<Item = (String, String)>,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    T::deserialize(Deserializer::new(iter.into_iter())).map_err(|error| match error {
 | 
				
			||||||
 | 
					        Error::MissingValue(value) => Error::MissingValue(value.to_uppercase()),
 | 
				
			||||||
 | 
					        _ => error,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A type which filters env vars with a prefix for use as serde field inputs
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// These types are created with with the [prefixed](fn.prefixed.html) module function
 | 
				
			||||||
 | 
					pub struct Prefixed<'a>(Cow<'a, str>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> Prefixed<'a> {
 | 
				
			||||||
 | 
					    /// Deserializes a type based on prefixed env variables
 | 
				
			||||||
 | 
					    pub fn from_env<T>(&self) -> Result<T>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        T: de::DeserializeOwned,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        self.from_iter(env::vars())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Deserializes a type based on prefixed (String, String) tuples
 | 
				
			||||||
 | 
					    pub fn from_iter<Iter, T>(&self, iter: Iter) -> Result<T>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        T: de::DeserializeOwned,
 | 
				
			||||||
 | 
					        Iter: IntoIterator<Item = (String, String)>,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        crate::from_iter(iter.into_iter().filter_map(|(k, v)| {
 | 
				
			||||||
 | 
					            if k.starts_with(self.0.as_ref()) {
 | 
				
			||||||
 | 
					                Some((k.trim_start_matches(self.0.as_ref()).to_owned(), v))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                None
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					        .map_err(|error| match error {
 | 
				
			||||||
 | 
					            Error::MissingValue(value) => Error::MissingValue(
 | 
				
			||||||
 | 
					                format!("{prefix}{value}", prefix = self.0, value = value).to_uppercase(),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            _ => error,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Produces a instance of `Prefixed` for prefixing env variable names
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// # Example
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// ```no_run
 | 
				
			||||||
 | 
					/// use serde::Deserialize;
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// #[derive(Deserialize, Debug)]
 | 
				
			||||||
 | 
					/// struct Config {
 | 
				
			||||||
 | 
					///     foo: u16,
 | 
				
			||||||
 | 
					///     bar: bool,
 | 
				
			||||||
 | 
					///     baz: String,
 | 
				
			||||||
 | 
					///     boom: Option<u64>,
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// // all env variables will be expected to be prefixed with APP_
 | 
				
			||||||
 | 
					/// // i.e. APP_FOO, APP_BAR, ect
 | 
				
			||||||
 | 
					/// match envy::prefixed("APP_").from_env::<Config>() {
 | 
				
			||||||
 | 
					///     Ok(config) => println!("{:#?}", config),
 | 
				
			||||||
 | 
					///     Err(error) => eprintln!("{:#?}", error),
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					pub fn prefixed<'a, C>(prefix: C) -> Prefixed<'a>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    C: Into<Cow<'a, str>>,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    Prefixed(prefix.into())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					    use serde::Deserialize;
 | 
				
			||||||
 | 
					    use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[derive(Default, Deserialize, Debug, PartialEq)]
 | 
				
			||||||
 | 
					    #[serde(rename_all = "lowercase")]
 | 
				
			||||||
 | 
					    pub enum Size {
 | 
				
			||||||
 | 
					        Small,
 | 
				
			||||||
 | 
					        #[default]
 | 
				
			||||||
 | 
					        Medium,
 | 
				
			||||||
 | 
					        Large,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn default_kaboom() -> u16 {
 | 
				
			||||||
 | 
					        8080
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[derive(Deserialize, Debug, PartialEq)]
 | 
				
			||||||
 | 
					    pub struct CustomNewType(u32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[derive(Deserialize, Debug, PartialEq)]
 | 
				
			||||||
 | 
					    pub struct Foo {
 | 
				
			||||||
 | 
					        bar: String,
 | 
				
			||||||
 | 
					        baz: bool,
 | 
				
			||||||
 | 
					        zoom: Option<u16>,
 | 
				
			||||||
 | 
					        doom: Vec<u64>,
 | 
				
			||||||
 | 
					        boom: Vec<String>,
 | 
				
			||||||
 | 
					        #[serde(default = "default_kaboom")]
 | 
				
			||||||
 | 
					        kaboom: u16,
 | 
				
			||||||
 | 
					        #[serde(default)]
 | 
				
			||||||
 | 
					        debug_mode: bool,
 | 
				
			||||||
 | 
					        #[serde(default)]
 | 
				
			||||||
 | 
					        size: Size,
 | 
				
			||||||
 | 
					        provided: Option<String>,
 | 
				
			||||||
 | 
					        newtype: CustomNewType,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn deserialize_from_iter() {
 | 
				
			||||||
 | 
					        let data = vec![
 | 
				
			||||||
 | 
					            (String::from("BAR"), String::from("test")),
 | 
				
			||||||
 | 
					            (String::from("BAZ"), String::from("true")),
 | 
				
			||||||
 | 
					            (String::from("DOOM"), String::from("1, 2, 3 ")),
 | 
				
			||||||
 | 
					            // Empty string should result in empty vector.
 | 
				
			||||||
 | 
					            (String::from("BOOM"), String::from("")),
 | 
				
			||||||
 | 
					            (String::from("SIZE"), String::from("small")),
 | 
				
			||||||
 | 
					            (String::from("PROVIDED"), String::from("test")),
 | 
				
			||||||
 | 
					            (String::from("NEWTYPE"), String::from("42")),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        match from_iter::<_, Foo>(data) {
 | 
				
			||||||
 | 
					            Ok(actual) => assert_eq!(
 | 
				
			||||||
 | 
					                actual,
 | 
				
			||||||
 | 
					                Foo {
 | 
				
			||||||
 | 
					                    bar: String::from("test"),
 | 
				
			||||||
 | 
					                    baz: true,
 | 
				
			||||||
 | 
					                    zoom: None,
 | 
				
			||||||
 | 
					                    doom: vec![1, 2, 3],
 | 
				
			||||||
 | 
					                    boom: vec![],
 | 
				
			||||||
 | 
					                    kaboom: 8080,
 | 
				
			||||||
 | 
					                    debug_mode: false,
 | 
				
			||||||
 | 
					                    size: Size::Small,
 | 
				
			||||||
 | 
					                    provided: Some(String::from("test")),
 | 
				
			||||||
 | 
					                    newtype: CustomNewType(42)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Err(e) => panic!("{:#?}", e),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn fails_with_missing_value() {
 | 
				
			||||||
 | 
					        let data = vec![
 | 
				
			||||||
 | 
					            (String::from("BAR"), String::from("test")),
 | 
				
			||||||
 | 
					            (String::from("BAZ"), String::from("true")),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        match from_iter::<_, Foo>(data) {
 | 
				
			||||||
 | 
					            Ok(_) => panic!("expected failure"),
 | 
				
			||||||
 | 
					            Err(e) => assert_eq!(e, Error::MissingValue("DOOM".into())),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn prefixed_fails_with_missing_value() {
 | 
				
			||||||
 | 
					        let data = vec![
 | 
				
			||||||
 | 
					            (String::from("PREFIX_BAR"), String::from("test")),
 | 
				
			||||||
 | 
					            (String::from("PREFIX_BAZ"), String::from("true")),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match prefixed("PREFIX_").from_iter::<_, Foo>(data) {
 | 
				
			||||||
 | 
					            Ok(_) => panic!("expected failure"),
 | 
				
			||||||
 | 
					            Err(e) => assert_eq!(e, Error::MissingValue("PREFIX_DOOM".into())),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn fails_with_invalid_type() {
 | 
				
			||||||
 | 
					        let data = vec![
 | 
				
			||||||
 | 
					            (String::from("BAR"), String::from("test")),
 | 
				
			||||||
 | 
					            (String::from("BAZ"), String::from("notabool")),
 | 
				
			||||||
 | 
					            (String::from("DOOM"), String::from("1,2,3")),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        match from_iter::<_, Foo>(data) {
 | 
				
			||||||
 | 
					            Ok(_) => panic!("expected failure"),
 | 
				
			||||||
 | 
					            Err(e) => assert_eq!(
 | 
				
			||||||
 | 
					                e,
 | 
				
			||||||
 | 
					                Error::Custom(String::from("provided string was not `true` or `false` while parsing value \'notabool\' provided by BAZ"))
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn deserializes_from_prefixed_fieldnames() {
 | 
				
			||||||
 | 
					        let data = vec![
 | 
				
			||||||
 | 
					            (String::from("APP_BAR"), String::from("test")),
 | 
				
			||||||
 | 
					            (String::from("APP_BAZ"), String::from("true")),
 | 
				
			||||||
 | 
					            (String::from("APP_DOOM"), String::from("")),
 | 
				
			||||||
 | 
					            (String::from("APP_BOOM"), String::from("4,5")),
 | 
				
			||||||
 | 
					            (String::from("APP_SIZE"), String::from("small")),
 | 
				
			||||||
 | 
					            (String::from("APP_PROVIDED"), String::from("test")),
 | 
				
			||||||
 | 
					            (String::from("APP_NEWTYPE"), String::from("42")),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        match prefixed("APP_").from_iter::<_, Foo>(data) {
 | 
				
			||||||
 | 
					            Ok(actual) => assert_eq!(
 | 
				
			||||||
 | 
					                actual,
 | 
				
			||||||
 | 
					                Foo {
 | 
				
			||||||
 | 
					                    bar: String::from("test"),
 | 
				
			||||||
 | 
					                    baz: true,
 | 
				
			||||||
 | 
					                    zoom: None,
 | 
				
			||||||
 | 
					                    doom: vec![],
 | 
				
			||||||
 | 
					                    boom: vec!["4".to_string(), "5".to_string()],
 | 
				
			||||||
 | 
					                    kaboom: 8080,
 | 
				
			||||||
 | 
					                    debug_mode: false,
 | 
				
			||||||
 | 
					                    size: Size::Small,
 | 
				
			||||||
 | 
					                    provided: Some(String::from("test")),
 | 
				
			||||||
 | 
					                    newtype: CustomNewType(42)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Err(e) => panic!("{:#?}", e),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn prefixed_strips_prefixes() {
 | 
				
			||||||
 | 
					        let mut expected = HashMap::new();
 | 
				
			||||||
 | 
					        expected.insert("foo".to_string(), "bar".to_string());
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            prefixed("PRE_").from_iter(vec![("PRE_FOO".to_string(), "bar".to_string())]),
 | 
				
			||||||
 | 
					            Ok(expected)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn prefixed_doesnt_parse_non_prefixed() {
 | 
				
			||||||
 | 
					        let mut expected = HashMap::new();
 | 
				
			||||||
 | 
					        expected.insert("foo".to_string(), 12);
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            prefixed("PRE_").from_iter(vec![
 | 
				
			||||||
 | 
					                ("FOO".to_string(), "asd".to_string()),
 | 
				
			||||||
 | 
					                ("PRE_FOO".to_string(), "12".to_string())
 | 
				
			||||||
 | 
					            ]),
 | 
				
			||||||
 | 
					            Ok(expected)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn deserialize_optional() {
 | 
				
			||||||
 | 
					        #[derive(Deserialize)]
 | 
				
			||||||
 | 
					        #[serde(default)]
 | 
				
			||||||
 | 
					        struct X {
 | 
				
			||||||
 | 
					            val: Option<u32>,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        impl Default for X {
 | 
				
			||||||
 | 
					            fn default() -> Self {
 | 
				
			||||||
 | 
					                Self { val: Some(123) }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let data = vec![(String::from("VAL"), String::from(""))];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let res = from_iter::<_, X>(data).unwrap();
 | 
				
			||||||
 | 
					        assert_eq!(res.val, None)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								resources/icon.opt.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								resources/icon.opt.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 13.229 13.229"><g aria-label="AV" style="font-size:10.5833px;line-height:1.25;stroke-width:.264583"><path d="m12.381 2.878-2.698 7.557H8.73L6.031 2.878h.995L8.73 7.725q.17.466.286.879.116.402.19.772.074-.37.19-.783.117-.413.287-.889l1.693-4.826Z" style="fill:#888;fill-opacity:1"/><path d="m1.158 10.435 2.699-7.557h.952l2.699 7.557h-.995L4.81 5.588q-.169-.466-.285-.879-.117-.402-.19-.772-.075.37-.191.783-.117.412-.286.889l-1.694 4.826Z" style="font-size:10.5833px;line-height:1.25;fill:#ddd;fill-opacity:1;stroke-width:.264583"/></g></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 619 B  | 
							
								
								
									
										56
									
								
								resources/icon.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								resources/icon.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,56 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
				
			||||||
 | 
					<!-- Created with Inkscape (http://www.inkscape.org/) -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svg
 | 
				
			||||||
 | 
					   width="50"
 | 
				
			||||||
 | 
					   height="50"
 | 
				
			||||||
 | 
					   viewBox="0 0 13.229166 13.229167"
 | 
				
			||||||
 | 
					   version="1.1"
 | 
				
			||||||
 | 
					   id="svg5"
 | 
				
			||||||
 | 
					   inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
 | 
				
			||||||
 | 
					   sodipodi:docname="logo.svg"
 | 
				
			||||||
 | 
					   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
				
			||||||
 | 
					   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
				
			||||||
 | 
					   xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					   xmlns:svg="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					  <sodipodi:namedview
 | 
				
			||||||
 | 
					     id="namedview7"
 | 
				
			||||||
 | 
					     pagecolor="#000000"
 | 
				
			||||||
 | 
					     bordercolor="#666666"
 | 
				
			||||||
 | 
					     borderopacity="1.0"
 | 
				
			||||||
 | 
					     inkscape:showpageshadow="2"
 | 
				
			||||||
 | 
					     inkscape:pageopacity="0"
 | 
				
			||||||
 | 
					     inkscape:pagecheckerboard="0"
 | 
				
			||||||
 | 
					     inkscape:deskcolor="#d1d1d1"
 | 
				
			||||||
 | 
					     inkscape:document-units="mm"
 | 
				
			||||||
 | 
					     showgrid="false"
 | 
				
			||||||
 | 
					     inkscape:zoom="10.982338"
 | 
				
			||||||
 | 
					     inkscape:cx="3.8243224"
 | 
				
			||||||
 | 
					     inkscape:cy="29.046639"
 | 
				
			||||||
 | 
					     inkscape:window-width="2516"
 | 
				
			||||||
 | 
					     inkscape:window-height="1051"
 | 
				
			||||||
 | 
					     inkscape:window-x="0"
 | 
				
			||||||
 | 
					     inkscape:window-y="0"
 | 
				
			||||||
 | 
					     inkscape:window-maximized="1"
 | 
				
			||||||
 | 
					     inkscape:current-layer="text236" />
 | 
				
			||||||
 | 
					  <defs
 | 
				
			||||||
 | 
					     id="defs2" />
 | 
				
			||||||
 | 
					  <g
 | 
				
			||||||
 | 
					     inkscape:label="Layer 1"
 | 
				
			||||||
 | 
					     inkscape:groupmode="layer"
 | 
				
			||||||
 | 
					     id="layer1">
 | 
				
			||||||
 | 
					    <g
 | 
				
			||||||
 | 
					       aria-label="AV"
 | 
				
			||||||
 | 
					       id="text236"
 | 
				
			||||||
 | 
					       style="font-size:10.5833px;line-height:1.25;stroke-width:0.264583">
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         d="M 12.381365,2.8782164 9.6826233,10.434692 H 8.7301265 L 6.031385,2.8782164 H 7.0262152 L 8.7301265,7.7253677 Q 8.8994592,8.1910329 9.0158755,8.6037815 9.1322918,9.0059469 9.2063749,9.3763624 9.280458,9.0059469 9.3968743,8.5931982 9.5132903,8.1804496 9.6826233,7.7042011 L 11.375951,2.8782164 Z"
 | 
				
			||||||
 | 
					         style="fill:#888888;fill-opacity:1"
 | 
				
			||||||
 | 
					         id="path402" />
 | 
				
			||||||
 | 
					      <path
 | 
				
			||||||
 | 
					         d="M 1.1580623,10.434692 3.8568039,2.8782162 H 4.8093007 L 7.5080423,10.434692 H 6.513212 L 4.8093007,5.5875401 Q 4.639968,5.1218752 4.5235518,4.7091272 4.4071354,4.3069612 4.3330523,3.9365462 4.2589693,4.3069612 4.1425529,4.7197102 4.0261369,5.1324582 3.8568039,5.6087071 L 2.1634763,10.434692 Z"
 | 
				
			||||||
 | 
					         style="font-size:10.5833px;line-height:1.25;fill:#dddddd;fill-opacity:1;stroke-width:0.264583"
 | 
				
			||||||
 | 
					         id="path402-3" />
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					  </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										410
									
								
								src/app.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								src/app.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,410 @@
 | 
				
			||||||
 | 
					use std::{ops::Bound, path::PathBuf, sync::Arc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use async_zip::tokio::read::ZipEntryReader;
 | 
				
			||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    body::Body,
 | 
				
			||||||
 | 
					    extract::{Host, Request, State},
 | 
				
			||||||
 | 
					    http::{Response, Uri},
 | 
				
			||||||
 | 
					    response::{IntoResponse, Redirect},
 | 
				
			||||||
 | 
					    routing::{any, get, post},
 | 
				
			||||||
 | 
					    Form, Json, Router,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use headers::HeaderMapExt;
 | 
				
			||||||
 | 
					use http::{HeaderMap, StatusCode};
 | 
				
			||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					use tokio::{
 | 
				
			||||||
 | 
					    fs::File,
 | 
				
			||||||
 | 
					    io::{AsyncBufReadExt, AsyncReadExt, BufReader},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use tokio_util::{
 | 
				
			||||||
 | 
					    compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt},
 | 
				
			||||||
 | 
					    io::ReaderStream,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use tower_http::trace::{DefaultOnResponse, TraceLayer};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    artifact_api::{Artifact, ArtifactApi, ArtifactOrRun},
 | 
				
			||||||
 | 
					    cache::{Cache, CacheEntry, GetEntryResult, GetFileResult, GetFileResultFile, IndexEntry},
 | 
				
			||||||
 | 
					    config::Config,
 | 
				
			||||||
 | 
					    error::{Error, Result},
 | 
				
			||||||
 | 
					    gzip_reader::{PrecompressedGzipReader, GZIP_EXTRA_LEN},
 | 
				
			||||||
 | 
					    query::Query,
 | 
				
			||||||
 | 
					    templates::{self, LinkItem},
 | 
				
			||||||
 | 
					    util::{self, InsertTypedHeader},
 | 
				
			||||||
 | 
					    App,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					struct AppState {
 | 
				
			||||||
 | 
					    i: Arc<AppInner>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct AppInner {
 | 
				
			||||||
 | 
					    cfg: Config,
 | 
				
			||||||
 | 
					    cache: Cache,
 | 
				
			||||||
 | 
					    api: ArtifactApi,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Default for App {
 | 
				
			||||||
 | 
					    fn default() -> Self {
 | 
				
			||||||
 | 
					        Self::new()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct UrlForm {
 | 
				
			||||||
 | 
					    url: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl App {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        Self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn new_state(&self) -> AppState {
 | 
				
			||||||
 | 
					        AppState::new()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn run(&self) -> Result<()> {
 | 
				
			||||||
 | 
					        let address = "0.0.0.0:3000";
 | 
				
			||||||
 | 
					        let listener = tokio::net::TcpListener::bind(address).await?;
 | 
				
			||||||
 | 
					        tracing::info!("Listening on http://{address}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let router = Router::new()
 | 
				
			||||||
 | 
					            // Prevent search indexing since artifactview serves temporary artifacts
 | 
				
			||||||
 | 
					            .route(
 | 
				
			||||||
 | 
					                "/robots.txt",
 | 
				
			||||||
 | 
					                get(|| async { "User-agent: *\nDisallow: /\n" }),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            // Put the API in the .well-known folder, since it is disabled for pages
 | 
				
			||||||
 | 
					            .route("/.well-known/api/artifacts", get(Self::get_artifacts))
 | 
				
			||||||
 | 
					            .route("/.well-known/api/artifact", get(Self::get_artifact))
 | 
				
			||||||
 | 
					            .route("/.well-known/api/files", get(Self::get_files))
 | 
				
			||||||
 | 
					            // Prevent access to the .well-known folder since it enables abuse
 | 
				
			||||||
 | 
					            // (e.g. SSL certificate registration by an attacker)
 | 
				
			||||||
 | 
					            .route("/.well-known/*path", any(|| async { Error::Inaccessible }))
 | 
				
			||||||
 | 
					            // Serve artifact pages
 | 
				
			||||||
 | 
					            .route("/", get(Self::get_page))
 | 
				
			||||||
 | 
					            .route("/", post(Self::post_homepage))
 | 
				
			||||||
 | 
					            .fallback(get(Self::get_page))
 | 
				
			||||||
 | 
					            .with_state(self.new_state())
 | 
				
			||||||
 | 
					            // Log requests
 | 
				
			||||||
 | 
					            .layer(
 | 
				
			||||||
 | 
					                TraceLayer::new_for_http()
 | 
				
			||||||
 | 
					                    .make_span_with(|request: &Request<Body>| {
 | 
				
			||||||
 | 
					                        tracing::error_span!("request", url = util::full_url_from_request(request),)
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        axum::serve(listener, router).await?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn get_page(
 | 
				
			||||||
 | 
					        State(state): State<AppState>,
 | 
				
			||||||
 | 
					        Host(host): Host,
 | 
				
			||||||
 | 
					        uri: Uri,
 | 
				
			||||||
 | 
					        request: Request,
 | 
				
			||||||
 | 
					    ) -> Result<Response<Body>> {
 | 
				
			||||||
 | 
					        let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if subdomain.is_empty() {
 | 
				
			||||||
 | 
					            // Main page
 | 
				
			||||||
 | 
					            if uri.path() != "/" {
 | 
				
			||||||
 | 
					                return Err(Error::NotFound("path".into()));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Ok(Response::builder()
 | 
				
			||||||
 | 
					                .typed_header(headers::ContentType::html())
 | 
				
			||||||
 | 
					                .body(templates::Index::default().to_string().into())?)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let query = Query::from_subdomain(subdomain)?;
 | 
				
			||||||
 | 
					            let path = percent_encoding::percent_decode_str(uri.path()).decode_utf8_lossy();
 | 
				
			||||||
 | 
					            let hdrs = request.headers();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let res = state.i.cache.get_entry(&state.i.api, &query).await?;
 | 
				
			||||||
 | 
					            match res {
 | 
				
			||||||
 | 
					                GetEntryResult::Entry { entry, zip_path } => {
 | 
				
			||||||
 | 
					                    match entry.get_file(&path, uri.query().unwrap_or_default())? {
 | 
				
			||||||
 | 
					                        GetFileResult::File(res) => {
 | 
				
			||||||
 | 
					                            Self::serve_artifact_file(state, entry, zip_path, res, hdrs).await
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        GetFileResult::Listing(listing) => {
 | 
				
			||||||
 | 
					                            if !path.ends_with('/') {
 | 
				
			||||||
 | 
					                                return Ok(Redirect::to(&format!("{path}/")).into_response());
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            // TODO: store actual artifact names
 | 
				
			||||||
 | 
					                            let artifact_name = format!("A{}", query.artifact.unwrap());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            let mut path_components = vec![
 | 
				
			||||||
 | 
					                                LinkItem {
 | 
				
			||||||
 | 
					                                    name: query.shortid(),
 | 
				
			||||||
 | 
					                                    url: state
 | 
				
			||||||
 | 
					                                        .i
 | 
				
			||||||
 | 
					                                        .cfg
 | 
				
			||||||
 | 
					                                        .url_with_subdomain(&query.subdomain_with_artifact(None)),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                LinkItem {
 | 
				
			||||||
 | 
					                                    name: artifact_name.to_owned(),
 | 
				
			||||||
 | 
					                                    url: "/".to_string(),
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            ];
 | 
				
			||||||
 | 
					                            let mut buf = String::new();
 | 
				
			||||||
 | 
					                            for s in path.split('/').filter(|s| !s.is_empty()) {
 | 
				
			||||||
 | 
					                                buf.push('/');
 | 
				
			||||||
 | 
					                                buf += s;
 | 
				
			||||||
 | 
					                                path_components.push(LinkItem {
 | 
				
			||||||
 | 
					                                    name: s.to_owned(),
 | 
				
			||||||
 | 
					                                    url: buf.clone(),
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            let tmpl = templates::Listing {
 | 
				
			||||||
 | 
					                                main_url: state.i.cfg.main_url(),
 | 
				
			||||||
 | 
					                                version: templates::Version,
 | 
				
			||||||
 | 
					                                artifact_name: &artifact_name,
 | 
				
			||||||
 | 
					                                path_components,
 | 
				
			||||||
 | 
					                                n_dirs: listing.n_dirs,
 | 
				
			||||||
 | 
					                                n_files: listing.n_files,
 | 
				
			||||||
 | 
					                                has_parent: listing.has_parent,
 | 
				
			||||||
 | 
					                                entries: listing.entries,
 | 
				
			||||||
 | 
					                            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Ok(Response::builder()
 | 
				
			||||||
 | 
					                                .typed_header(headers::ContentType::html())
 | 
				
			||||||
 | 
					                                .body(tmpl.to_string().into())?)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                GetEntryResult::Artifacts(artifacts) => {
 | 
				
			||||||
 | 
					                    if uri.path() != "/" {
 | 
				
			||||||
 | 
					                        return Err(Error::NotFound("path".into()));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    if artifacts.is_empty() {
 | 
				
			||||||
 | 
					                        return Err(Error::NotFound("artifacts".into()));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    let tmpl = templates::Selection {
 | 
				
			||||||
 | 
					                        main_url: state.i.cfg.main_url(),
 | 
				
			||||||
 | 
					                        run_url: &query.forge_url(),
 | 
				
			||||||
 | 
					                        run_name: &query.shortid(),
 | 
				
			||||||
 | 
					                        artifacts: artifacts
 | 
				
			||||||
 | 
					                            .into_iter()
 | 
				
			||||||
 | 
					                            .map(|a| LinkItem::from_artifact(a, &query, &state.i.cfg))
 | 
				
			||||||
 | 
					                            .collect(),
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    Ok(Response::builder()
 | 
				
			||||||
 | 
					                        .typed_header(headers::ContentType::html())
 | 
				
			||||||
 | 
					                        .body(tmpl.to_string().into())?)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn post_homepage(
 | 
				
			||||||
 | 
					        State(state): State<AppState>,
 | 
				
			||||||
 | 
					        Host(host): Host,
 | 
				
			||||||
 | 
					        Form(url): Form<UrlForm>,
 | 
				
			||||||
 | 
					    ) -> Result<Redirect> {
 | 
				
			||||||
 | 
					        let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if subdomain.is_empty() {
 | 
				
			||||||
 | 
					            let query = Query::from_forge_url(&url.url)?;
 | 
				
			||||||
 | 
					            let subdomain = query.subdomain();
 | 
				
			||||||
 | 
					            let target = format!(
 | 
				
			||||||
 | 
					                "{}{}.{}",
 | 
				
			||||||
 | 
					                state.i.cfg.url_proto(),
 | 
				
			||||||
 | 
					                subdomain,
 | 
				
			||||||
 | 
					                state.i.cfg.load().root_domain
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            Ok(Redirect::to(&target))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Err(Error::MethodNotAllowed)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn serve_artifact_file(
 | 
				
			||||||
 | 
					        state: AppState,
 | 
				
			||||||
 | 
					        entry: Arc<CacheEntry>,
 | 
				
			||||||
 | 
					        zip_path: PathBuf,
 | 
				
			||||||
 | 
					        res: GetFileResultFile,
 | 
				
			||||||
 | 
					        hdrs: &HeaderMap,
 | 
				
			||||||
 | 
					    ) -> Result<Response<Body>> {
 | 
				
			||||||
 | 
					        let file = res.file;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Dont serve files above the configured size limit
 | 
				
			||||||
 | 
					        let lim = state.i.cfg.load().max_file_size;
 | 
				
			||||||
 | 
					        if lim.is_some_and(|lim| file.uncompressed_size > lim) {
 | 
				
			||||||
 | 
					            return Err(Error::BadRequest(
 | 
				
			||||||
 | 
					                format!(
 | 
				
			||||||
 | 
					                    "file too large (size: {}, limit: {})",
 | 
				
			||||||
 | 
					                    file.uncompressed_size,
 | 
				
			||||||
 | 
					                    lim.unwrap()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .into(),
 | 
				
			||||||
 | 
					            ));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut resp = Response::builder()
 | 
				
			||||||
 | 
					            .status(res.status)
 | 
				
			||||||
 | 
					            .typed_header(headers::AcceptRanges::bytes());
 | 
				
			||||||
 | 
					        if let Some(mime) = res.mime {
 | 
				
			||||||
 | 
					            resp = resp.typed_header(headers::ContentType::from(mime));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let Some(last_mod) = entry.last_modified {
 | 
				
			||||||
 | 
					            resp = resp.typed_header(headers::LastModified::from(last_mod));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // handle if-(un)modified queries
 | 
				
			||||||
 | 
					        if let Some(modified) = entry.last_modified {
 | 
				
			||||||
 | 
					            if let Some(if_unmodified_since) = hdrs.typed_get::<headers::IfUnmodifiedSince>() {
 | 
				
			||||||
 | 
					                if !if_unmodified_since.precondition_passes(modified) {
 | 
				
			||||||
 | 
					                    return Ok(resp
 | 
				
			||||||
 | 
					                        .status(StatusCode::PRECONDITION_FAILED)
 | 
				
			||||||
 | 
					                        .body(Body::empty())?);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if let Some(if_modified_since) = hdrs.typed_get::<headers::IfModifiedSince>() {
 | 
				
			||||||
 | 
					                if !if_modified_since.is_modified(modified) {
 | 
				
			||||||
 | 
					                    return Ok(resp.status(StatusCode::NOT_MODIFIED).body(Body::empty())?);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let zip_file = File::open(&zip_path).await?;
 | 
				
			||||||
 | 
					        let range = hdrs.typed_get::<headers::Range>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if matches!(file.compression, async_zip::Compression::Deflate)
 | 
				
			||||||
 | 
					            && range.is_none()
 | 
				
			||||||
 | 
					            && util::accepts_gzip(hdrs)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Read compressed file
 | 
				
			||||||
 | 
					            let reader = PrecompressedGzipReader::new(zip_file, &file).await?;
 | 
				
			||||||
 | 
					            resp = resp
 | 
				
			||||||
 | 
					                .typed_header(headers::ContentLength(
 | 
				
			||||||
 | 
					                    u64::from(file.compressed_size) + GZIP_EXTRA_LEN,
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
 | 
					                .typed_header(headers::ContentEncoding::gzip());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(resp.body(Body::from_stream(ReaderStream::new(reader)))?)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Read decompressed file
 | 
				
			||||||
 | 
					            let mut zip_reader = BufReader::new(zip_file);
 | 
				
			||||||
 | 
					            util::seek_to_data_offset(&mut zip_reader, file.header_offset.into()).await?;
 | 
				
			||||||
 | 
					            let reader = ZipEntryReader::new_with_owned(
 | 
				
			||||||
 | 
					                zip_reader.compat(),
 | 
				
			||||||
 | 
					                file.compression,
 | 
				
			||||||
 | 
					                file.compressed_size.into(),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if let Some(rheader) = range {
 | 
				
			||||||
 | 
					                let total_len = u64::from(file.uncompressed_size);
 | 
				
			||||||
 | 
					                let mut ranges = rheader.satisfiable_ranges(total_len);
 | 
				
			||||||
 | 
					                if let Some(range) = ranges.next() {
 | 
				
			||||||
 | 
					                    if ranges.next().is_some() {
 | 
				
			||||||
 | 
					                        return Err(Error::BadRequest(
 | 
				
			||||||
 | 
					                            "multipart ranges are not implemented".into(),
 | 
				
			||||||
 | 
					                        ));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    let start = match range.0 {
 | 
				
			||||||
 | 
					                        Bound::Included(n) => n,
 | 
				
			||||||
 | 
					                        Bound::Excluded(n) => n + 1,
 | 
				
			||||||
 | 
					                        Bound::Unbounded => 0,
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					                    let end = match range.1 {
 | 
				
			||||||
 | 
					                        Bound::Included(n) => n + 1,
 | 
				
			||||||
 | 
					                        Bound::Excluded(n) => n,
 | 
				
			||||||
 | 
					                        Bound::Unbounded => total_len,
 | 
				
			||||||
 | 
					                    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let mut bufreader = tokio::io::BufReader::new(reader.compat());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Advance the BufReader by the parsed offset
 | 
				
			||||||
 | 
					                    let mut to_consume = usize::try_from(start)?;
 | 
				
			||||||
 | 
					                    while to_consume > 0 {
 | 
				
			||||||
 | 
					                        let take = bufreader.fill_buf().await?.len().min(to_consume);
 | 
				
			||||||
 | 
					                        bufreader.consume(take);
 | 
				
			||||||
 | 
					                        to_consume -= take;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let content_length = end - start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return Ok(resp
 | 
				
			||||||
 | 
					                        .status(StatusCode::PARTIAL_CONTENT)
 | 
				
			||||||
 | 
					                        .typed_header(headers::ContentLength(content_length))
 | 
				
			||||||
 | 
					                        .typed_header(
 | 
				
			||||||
 | 
					                            headers::ContentRange::bytes(range, total_len)
 | 
				
			||||||
 | 
					                                .map_err(|e| Error::Internal(e.to_string().into()))?,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .body(Body::from_stream(ReaderStream::new(
 | 
				
			||||||
 | 
					                            bufreader.take(content_length),
 | 
				
			||||||
 | 
					                        )))?);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Ok(resp
 | 
				
			||||||
 | 
					                .typed_header(headers::ContentLength(file.uncompressed_size.into()))
 | 
				
			||||||
 | 
					                .body(Body::from_stream(ReaderStream::new(reader.compat())))?)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// API endpoint to list artifacts of a CI run
 | 
				
			||||||
 | 
					    async fn get_artifacts(
 | 
				
			||||||
 | 
					        State(state): State<AppState>,
 | 
				
			||||||
 | 
					        Host(host): Host,
 | 
				
			||||||
 | 
					    ) -> Result<Json<Vec<Artifact>>> {
 | 
				
			||||||
 | 
					        let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
 | 
				
			||||||
 | 
					        let query = Query::from_subdomain(subdomain)?;
 | 
				
			||||||
 | 
					        let artifacts = state.i.api.list(&query).await?;
 | 
				
			||||||
 | 
					        Ok(Json(artifacts))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// API endpoint to get the metadata of the current artifact
 | 
				
			||||||
 | 
					    async fn get_artifact(
 | 
				
			||||||
 | 
					        State(state): State<AppState>,
 | 
				
			||||||
 | 
					        Host(host): Host,
 | 
				
			||||||
 | 
					    ) -> Result<Json<Artifact>> {
 | 
				
			||||||
 | 
					        let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
 | 
				
			||||||
 | 
					        let query = Query::from_subdomain(subdomain)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if query.artifact.is_none() {
 | 
				
			||||||
 | 
					            return Err(Error::BadRequest("no artifact specified".into()));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let artifact = state.i.api.fetch(&query).await?;
 | 
				
			||||||
 | 
					        match artifact {
 | 
				
			||||||
 | 
					            ArtifactOrRun::Artifact(artifact) => Ok(Json(artifact)),
 | 
				
			||||||
 | 
					            ArtifactOrRun::Run(_) => unreachable!(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// API endpoint to get a file listing
 | 
				
			||||||
 | 
					    async fn get_files(
 | 
				
			||||||
 | 
					        State(state): State<AppState>,
 | 
				
			||||||
 | 
					        Host(host): Host,
 | 
				
			||||||
 | 
					    ) -> Result<Json<Vec<IndexEntry>>> {
 | 
				
			||||||
 | 
					        let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
 | 
				
			||||||
 | 
					        let query = Query::from_subdomain(subdomain)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if query.artifact.is_none() {
 | 
				
			||||||
 | 
					            return Err(Error::BadRequest("no artifact specified".into()));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let res = state.i.cache.get_entry(&state.i.api, &query).await?;
 | 
				
			||||||
 | 
					        let entry = match res {
 | 
				
			||||||
 | 
					            GetEntryResult::Entry { entry, .. } => entry,
 | 
				
			||||||
 | 
					            GetEntryResult::Artifacts(_) => unreachable!(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let files = entry.get_files();
 | 
				
			||||||
 | 
					        Ok(Json(files))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl AppState {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        let cfg = Config::default();
 | 
				
			||||||
 | 
					        let cache = Cache::new(cfg.clone());
 | 
				
			||||||
 | 
					        let api = ArtifactApi::new(cfg.clone());
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            i: Arc::new(AppInner { cfg, cache, api }),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,16 @@
 | 
				
			||||||
//! API-Client to fetch CI artifacts from Github and Forgejo
 | 
					//! API-Client to fetch CI artifacts from Github and Forgejo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use anyhow::{anyhow, Result};
 | 
					use std::{fs::File, io::Cursor, path::Path};
 | 
				
			||||||
use reqwest::{header, Client, ClientBuilder, IntoUrl, RequestBuilder};
 | 
					
 | 
				
			||||||
 | 
					use http::header;
 | 
				
			||||||
 | 
					use reqwest::{Client, ClientBuilder, IntoUrl, RequestBuilder, Url};
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{config::Config, query::Query};
 | 
					use crate::{
 | 
				
			||||||
 | 
					    config::Config,
 | 
				
			||||||
 | 
					    error::{Error, Result},
 | 
				
			||||||
 | 
					    query::Query,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct ArtifactApi {
 | 
					pub struct ArtifactApi {
 | 
				
			||||||
    http: Client,
 | 
					    http: Client,
 | 
				
			||||||
| 
						 | 
					@ -20,6 +26,11 @@ pub struct Artifact {
 | 
				
			||||||
    pub download_url: String,
 | 
					    pub download_url: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum ArtifactOrRun {
 | 
				
			||||||
 | 
					    Artifact(Artifact),
 | 
				
			||||||
 | 
					    Run(Vec<Artifact>),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Deserialize)]
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
struct GithubArtifact {
 | 
					struct GithubArtifact {
 | 
				
			||||||
    id: u64,
 | 
					    id: u64,
 | 
				
			||||||
| 
						 | 
					@ -61,7 +72,7 @@ impl From<GithubArtifact> for Artifact {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl ForgejoArtifact {
 | 
					impl ForgejoArtifact {
 | 
				
			||||||
    fn to_artifact(self, id: u64, query: &Query) -> Artifact {
 | 
					    fn into_artifact(self, id: u64, query: &Query) -> Artifact {
 | 
				
			||||||
        Artifact {
 | 
					        Artifact {
 | 
				
			||||||
            download_url: format!(
 | 
					            download_url: format!(
 | 
				
			||||||
                "https://{}/{}/{}/actions/runs/{}/artifacts/{}",
 | 
					                "https://{}/{}/{}/actions/runs/{}/artifacts/{}",
 | 
				
			||||||
| 
						 | 
					@ -92,24 +103,74 @@ impl ArtifactApi {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn list(&self, query: &Query) -> Result<Vec<Artifact>> {
 | 
					    pub async fn list(&self, query: &Query) -> Result<Vec<Artifact>> {
 | 
				
			||||||
        if query.is_github() {
 | 
					        if query.is_github() {
 | 
				
			||||||
            self.list_forgejo(query).await
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            self.list_github(query).await
 | 
					            self.list_github(query).await
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            self.list_forgejo(query).await
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn fetch(&self, query: &Query) -> Result<Artifact> {
 | 
					    pub async fn fetch(&self, query: &Query) -> Result<ArtifactOrRun> {
 | 
				
			||||||
        if query.is_github() {
 | 
					        if query.is_github() {
 | 
				
			||||||
            self.fetch_github(query).await
 | 
					            self.fetch_github(query).await
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // Forgejo currently has no API for fetching single artifacts
 | 
					            // Forgejo currently has no API for fetching single artifacts
 | 
				
			||||||
            let mut artifacts = self.list_forgejo(query).await?;
 | 
					            let mut artifacts = self.list_forgejo(query).await?;
 | 
				
			||||||
            let i = usize::try_from(query.artifact)?;
 | 
					
 | 
				
			||||||
 | 
					            match query.artifact {
 | 
				
			||||||
 | 
					                Some(artifact) => {
 | 
				
			||||||
 | 
					                    let i = usize::try_from(artifact)?;
 | 
				
			||||||
                    if i == 0 || i > artifacts.len() {
 | 
					                    if i == 0 || i > artifacts.len() {
 | 
				
			||||||
                return Err(anyhow!("Artifact not found"));
 | 
					                        return Err(Error::NotFound("artifact".into()));
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
            Ok(artifacts.swap_remove(i - 1))
 | 
					                    Ok(ArtifactOrRun::Artifact(artifacts.swap_remove(i - 1)))
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                None => Ok(ArtifactOrRun::Run(artifacts)),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn download(&self, artifact: &Artifact, path: &Path) -> Result<()> {
 | 
				
			||||||
 | 
					        if artifact.expired {
 | 
				
			||||||
 | 
					            return Err(Error::Expired);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let lim = self.cfg.load().max_artifact_size;
 | 
				
			||||||
 | 
					        let check_lim = |size: u64| {
 | 
				
			||||||
 | 
					            if lim.is_some_and(|lim| u32::try_from(size).map(|size| size > lim).unwrap_or(true)) {
 | 
				
			||||||
 | 
					                Err(Error::BadRequest(
 | 
				
			||||||
 | 
					                    format!(
 | 
				
			||||||
 | 
					                        "artifact too large (size: {}, limit: {})",
 | 
				
			||||||
 | 
					                        artifact.size,
 | 
				
			||||||
 | 
					                        lim.unwrap()
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .into(),
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Ok(())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        check_lim(artifact.size)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let url = Url::parse(&artifact.download_url)?;
 | 
				
			||||||
 | 
					        let req = if url.domain() == Some("api.github.com") {
 | 
				
			||||||
 | 
					            self.get_github(url)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            self.http.get(url)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let resp = req.send().await?.error_for_status()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(act_len) = resp.content_length() {
 | 
				
			||||||
 | 
					            check_lim(act_len)?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let tmp_path = path.with_extension(format!("tmp.{:x}", rand::random::<u32>()));
 | 
				
			||||||
 | 
					        let mut file = File::create(&tmp_path)?;
 | 
				
			||||||
 | 
					        let mut content = Cursor::new(resp.bytes().await?);
 | 
				
			||||||
 | 
					        std::io::copy(&mut content, &mut file)?;
 | 
				
			||||||
 | 
					        std::fs::rename(&tmp_path, path)?;
 | 
				
			||||||
 | 
					        tracing::info!("Downloaded artifact from {}", artifact.download_url);
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn list_forgejo(&self, query: &Query) -> Result<Vec<Artifact>> {
 | 
					    async fn list_forgejo(&self, query: &Query) -> Result<Vec<Artifact>> {
 | 
				
			||||||
| 
						 | 
					@ -131,7 +192,7 @@ impl ArtifactApi {
 | 
				
			||||||
            .artifacts
 | 
					            .artifacts
 | 
				
			||||||
            .into_iter()
 | 
					            .into_iter()
 | 
				
			||||||
            .enumerate()
 | 
					            .enumerate()
 | 
				
			||||||
            .map(|(i, a)| a.to_artifact(i as u64 + 1, query))
 | 
					            .map(|(i, a)| a.into_artifact(i as u64 + 1, query))
 | 
				
			||||||
            .collect::<Vec<_>>();
 | 
					            .collect::<Vec<_>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(artifacts)
 | 
					        Ok(artifacts)
 | 
				
			||||||
| 
						 | 
					@ -154,10 +215,12 @@ impl ArtifactApi {
 | 
				
			||||||
        Ok(resp.artifacts.into_iter().map(Artifact::from).collect())
 | 
					        Ok(resp.artifacts.into_iter().map(Artifact::from).collect())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn fetch_github(&self, query: &Query) -> Result<Artifact> {
 | 
					    async fn fetch_github(&self, query: &Query) -> Result<ArtifactOrRun> {
 | 
				
			||||||
 | 
					        match query.artifact {
 | 
				
			||||||
 | 
					            Some(artifact) => {
 | 
				
			||||||
                let url = format!(
 | 
					                let url = format!(
 | 
				
			||||||
                    "https://api.github.com/repos/{}/{}/actions/artifacts/{}",
 | 
					                    "https://api.github.com/repos/{}/{}/actions/artifacts/{}",
 | 
				
			||||||
            query.user, query.repo, query.artifact
 | 
					                    query.user, query.repo, artifact
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let artifact = self
 | 
					                let artifact = self
 | 
				
			||||||
| 
						 | 
					@ -167,8 +230,10 @@ impl ArtifactApi {
 | 
				
			||||||
                    .error_for_status()?
 | 
					                    .error_for_status()?
 | 
				
			||||||
                    .json::<GithubArtifact>()
 | 
					                    .json::<GithubArtifact>()
 | 
				
			||||||
                    .await?;
 | 
					                    .await?;
 | 
				
			||||||
 | 
					                Ok(ArtifactOrRun::Artifact(artifact.into()))
 | 
				
			||||||
        Ok(artifact.into())
 | 
					            }
 | 
				
			||||||
 | 
					            None => Ok(ArtifactOrRun::Run(self.list_github(query).await?)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn get_github<U: IntoUrl>(&self, url: U) -> RequestBuilder {
 | 
					    fn get_github<U: IntoUrl>(&self, url: U) -> RequestBuilder {
 | 
				
			||||||
| 
						 | 
					@ -185,7 +250,7 @@ impl ArtifactApi {
 | 
				
			||||||
mod tests {
 | 
					mod tests {
 | 
				
			||||||
    use crate::{config::Config, query::Query};
 | 
					    use crate::{config::Config, query::Query};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use super::ArtifactApi;
 | 
					    use super::{ArtifactApi, ArtifactOrRun};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[tokio::test]
 | 
					    #[tokio::test]
 | 
				
			||||||
    async fn fetch_forgejo() {
 | 
					    async fn fetch_forgejo() {
 | 
				
			||||||
| 
						 | 
					@ -194,14 +259,22 @@ mod tests {
 | 
				
			||||||
            user: "HSA".to_owned(),
 | 
					            user: "HSA".to_owned(),
 | 
				
			||||||
            repo: "Visitenbuch".to_owned(),
 | 
					            repo: "Visitenbuch".to_owned(),
 | 
				
			||||||
            run: 32,
 | 
					            run: 32,
 | 
				
			||||||
            artifact: 1,
 | 
					            artifact: Some(1),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        let api = ArtifactApi::new(Config::default());
 | 
					        let api = ArtifactApi::new(Config::default());
 | 
				
			||||||
        let res = api.fetch(&query).await.unwrap();
 | 
					        let res = api.fetch(&query).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let ArtifactOrRun::Artifact(res) = res {
 | 
				
			||||||
            assert_eq!(res.name, "playwright-report");
 | 
					            assert_eq!(res.name, "playwright-report");
 | 
				
			||||||
        assert_eq!(res.download_url, "https://code.thetadev.de/HSA/Visitenbuch/actions/runs/32/artifacts/playwright-report");
 | 
					            assert_eq!(
 | 
				
			||||||
 | 
					            res.download_url,
 | 
				
			||||||
 | 
					            "https://code.thetadev.de/HSA/Visitenbuch/actions/runs/32/artifacts/playwright-report"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
            assert_eq!(res.id, 1);
 | 
					            assert_eq!(res.id, 1);
 | 
				
			||||||
            assert_eq!(res.size, 574292);
 | 
					            assert_eq!(res.size, 574292);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            panic!("got run");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[tokio::test]
 | 
					    #[tokio::test]
 | 
				
			||||||
| 
						 | 
					@ -211,13 +284,21 @@ mod tests {
 | 
				
			||||||
            user: "actions".to_owned(),
 | 
					            user: "actions".to_owned(),
 | 
				
			||||||
            repo: "upload-artifact".to_owned(),
 | 
					            repo: "upload-artifact".to_owned(),
 | 
				
			||||||
            run: 8805345396,
 | 
					            run: 8805345396,
 | 
				
			||||||
            artifact: 1440556464,
 | 
					            artifact: Some(1440556464),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        let api = ArtifactApi::new(Config::default());
 | 
					        let api = ArtifactApi::new(Config::default());
 | 
				
			||||||
        let res = api.fetch(&query).await.unwrap();
 | 
					        let res = api.fetch(&query).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let ArtifactOrRun::Artifact(res) = res {
 | 
				
			||||||
            assert_eq!(res.name, "Artifact-Wildcard-macos-latest");
 | 
					            assert_eq!(res.name, "Artifact-Wildcard-macos-latest");
 | 
				
			||||||
        assert_eq!(res.download_url, "https://api.github.com/repos/actions/upload-artifact/actions/artifacts/1440556464/zip");
 | 
					            assert_eq!(
 | 
				
			||||||
 | 
					            res.download_url,
 | 
				
			||||||
 | 
					            "https://api.github.com/repos/actions/upload-artifact/actions/artifacts/1440556464/zip"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
            assert_eq!(res.id, 1440556464);
 | 
					            assert_eq!(res.id, 1440556464);
 | 
				
			||||||
            assert_eq!(res.size, 334);
 | 
					            assert_eq!(res.size, 334);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            panic!("got run");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										317
									
								
								src/cache.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								src/cache.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,317 @@
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    borrow::Cow,
 | 
				
			||||||
 | 
					    collections::{BTreeMap, HashMap},
 | 
				
			||||||
 | 
					    path::{Path, PathBuf},
 | 
				
			||||||
 | 
					    sync::Arc,
 | 
				
			||||||
 | 
					    time::{Duration, SystemTime},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use async_zip::{tokio::read::fs::ZipFileReader, Compression};
 | 
				
			||||||
 | 
					use http::StatusCode;
 | 
				
			||||||
 | 
					use mime::Mime;
 | 
				
			||||||
 | 
					use path_macro::path;
 | 
				
			||||||
 | 
					use quick_cache::sync::Cache as QuickCache;
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use serde_hex::{SerHex, Strict};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    artifact_api::{Artifact, ArtifactApi, ArtifactOrRun},
 | 
				
			||||||
 | 
					    config::Config,
 | 
				
			||||||
 | 
					    error::{Error, Result},
 | 
				
			||||||
 | 
					    query::Query,
 | 
				
			||||||
 | 
					    util,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Cache {
 | 
				
			||||||
 | 
					    cfg: Config,
 | 
				
			||||||
 | 
					    qc: QuickCache<[u8; 16], Arc<CacheEntry>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct CacheEntry {
 | 
				
			||||||
 | 
					    pub files: HashMap<String, FileEntry>,
 | 
				
			||||||
 | 
					    pub last_modified: Option<SystemTime>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct FileEntry {
 | 
				
			||||||
 | 
					    pub header_offset: u32,
 | 
				
			||||||
 | 
					    pub uncompressed_size: u32,
 | 
				
			||||||
 | 
					    pub compressed_size: u32,
 | 
				
			||||||
 | 
					    pub crc32: u32,
 | 
				
			||||||
 | 
					    pub compression: Compression,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum GetEntryResult {
 | 
				
			||||||
 | 
					    Entry {
 | 
				
			||||||
 | 
					        entry: Arc<CacheEntry>,
 | 
				
			||||||
 | 
					        zip_path: PathBuf,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Artifacts(Vec<Artifact>),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum GetFileResult {
 | 
				
			||||||
 | 
					    File(GetFileResultFile),
 | 
				
			||||||
 | 
					    Listing(Listing),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct GetFileResultFile {
 | 
				
			||||||
 | 
					    pub file: FileEntry,
 | 
				
			||||||
 | 
					    pub mime: Option<Mime>,
 | 
				
			||||||
 | 
					    pub status: StatusCode,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize)]
 | 
				
			||||||
 | 
					pub struct IndexEntry {
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub size: u32,
 | 
				
			||||||
 | 
					    #[serde(with = "SerHex::<Strict>")]
 | 
				
			||||||
 | 
					    pub crc32: u32,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Listing {
 | 
				
			||||||
 | 
					    pub entries: Vec<ListingEntry>,
 | 
				
			||||||
 | 
					    pub n_files: usize,
 | 
				
			||||||
 | 
					    pub n_dirs: usize,
 | 
				
			||||||
 | 
					    pub has_parent: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ListingEntry {
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub url: String,
 | 
				
			||||||
 | 
					    pub size: u32,
 | 
				
			||||||
 | 
					    pub crc32: String,
 | 
				
			||||||
 | 
					    pub is_dir: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Cache {
 | 
				
			||||||
 | 
					    pub fn new(cfg: Config) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            cfg,
 | 
				
			||||||
 | 
					            qc: QuickCache::new(50),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_path(&self, query: &Query) -> PathBuf {
 | 
				
			||||||
 | 
					        path!(self.cfg.load().cache_dir / format!("{}.zip", hex::encode(query.siphash())))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn get_entry(&self, api: &ArtifactApi, query: &Query) -> Result<GetEntryResult> {
 | 
				
			||||||
 | 
					        if query.artifact.is_some() {
 | 
				
			||||||
 | 
					            let hash = query.siphash();
 | 
				
			||||||
 | 
					            let zip_path = path!(self.cfg.load().cache_dir / format!("{}.zip", hex::encode(hash)));
 | 
				
			||||||
 | 
					            if !zip_path.is_file() {
 | 
				
			||||||
 | 
					                let artifact = api.fetch(query).await?;
 | 
				
			||||||
 | 
					                let artifact = match artifact {
 | 
				
			||||||
 | 
					                    ArtifactOrRun::Artifact(artifact) => artifact,
 | 
				
			||||||
 | 
					                    ArtifactOrRun::Run(_) => unreachable!(),
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                api.download(&artifact, &zip_path).await?;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let timeout = self
 | 
				
			||||||
 | 
					                .cfg
 | 
				
			||||||
 | 
					                .load()
 | 
				
			||||||
 | 
					                .zip_timeout_ms
 | 
				
			||||||
 | 
					                .map(|t| Duration::from_millis(t.into()));
 | 
				
			||||||
 | 
					            let mut entry = self
 | 
				
			||||||
 | 
					                .qc
 | 
				
			||||||
 | 
					                .get_or_insert_async(&hash, async {
 | 
				
			||||||
 | 
					                    Ok::<_, Error>(Arc::new(CacheEntry::new(&zip_path, timeout).await?))
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Verify if the cached entry is fresh
 | 
				
			||||||
 | 
					            let meta = tokio::fs::metadata(&zip_path).await?;
 | 
				
			||||||
 | 
					            if meta.modified().ok() != entry.last_modified {
 | 
				
			||||||
 | 
					                tracing::info!("cached file {zip_path:?} changed");
 | 
				
			||||||
 | 
					                entry = Arc::new(CacheEntry::new(&zip_path, timeout).await?);
 | 
				
			||||||
 | 
					                self.qc.insert(hash, entry.clone());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Ok(GetEntryResult::Entry { entry, zip_path })
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let run = api.fetch(query).await?;
 | 
				
			||||||
 | 
					            let artifacts = match run {
 | 
				
			||||||
 | 
					                ArtifactOrRun::Artifact(_) => unreachable!(),
 | 
				
			||||||
 | 
					                ArtifactOrRun::Run(run) => run,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(GetEntryResult::Artifacts(artifacts))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl CacheEntry {
 | 
				
			||||||
 | 
					    async fn new(zip_path: &Path, timeout: Option<Duration>) -> Result<Self> {
 | 
				
			||||||
 | 
					        let meta = tokio::fs::metadata(&zip_path).await?;
 | 
				
			||||||
 | 
					        let zip_fut = ZipFileReader::new(&zip_path);
 | 
				
			||||||
 | 
					        let zip = match timeout {
 | 
				
			||||||
 | 
					            Some(timeout) => tokio::time::timeout(timeout, zip_fut).await??,
 | 
				
			||||||
 | 
					            None => zip_fut.await?,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            files: zip
 | 
				
			||||||
 | 
					                .file()
 | 
				
			||||||
 | 
					                .entries()
 | 
				
			||||||
 | 
					                .iter()
 | 
				
			||||||
 | 
					                .filter_map(|entry| {
 | 
				
			||||||
 | 
					                    Some((
 | 
				
			||||||
 | 
					                        entry.filename().as_str().ok()?.to_owned(),
 | 
				
			||||||
 | 
					                        FileEntry {
 | 
				
			||||||
 | 
					                            header_offset: entry.header_offset().try_into().ok()?,
 | 
				
			||||||
 | 
					                            uncompressed_size: entry.uncompressed_size().try_into().ok()?,
 | 
				
			||||||
 | 
					                            compressed_size: entry.compressed_size().try_into().ok()?,
 | 
				
			||||||
 | 
					                            crc32: entry.crc32(),
 | 
				
			||||||
 | 
					                            compression: entry.compression(),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ))
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect(),
 | 
				
			||||||
 | 
					            last_modified: meta.modified().ok(),
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_file(&self, path: &str, url_query: &str) -> Result<GetFileResult> {
 | 
				
			||||||
 | 
					        let path = path.trim_start_matches('/');
 | 
				
			||||||
 | 
					        let mut index_path: Option<Cow<str>> = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if path.is_empty() {
 | 
				
			||||||
 | 
					            // Special case: open index.html directly
 | 
				
			||||||
 | 
					            index_path = Some("index.html".into());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Attempt to access the following pages
 | 
				
			||||||
 | 
					        // 1. Site path directly
 | 
				
			||||||
 | 
					        // 2. Site path + `/index.html`
 | 
				
			||||||
 | 
					        else if let Some(file) = self.files.get(path) {
 | 
				
			||||||
 | 
					            return Ok(GetFileResult::File(GetFileResultFile {
 | 
				
			||||||
 | 
					                file: file.clone(),
 | 
				
			||||||
 | 
					                mime: util::path_mime(path),
 | 
				
			||||||
 | 
					                status: StatusCode::OK,
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
 | 
					        } else if util::site_path_ext(path).is_none() {
 | 
				
			||||||
 | 
					            index_path = Some(format!("{path}/index.html").into());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(file) = index_path
 | 
				
			||||||
 | 
					            .and_then(|p: Cow<str>| self.files.get(p.as_ref()))
 | 
				
			||||||
 | 
					            .or_else(|| self.files.get("200.html"))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // index.html or SPA entrypoint
 | 
				
			||||||
 | 
					            return Ok(GetFileResult::File(GetFileResultFile {
 | 
				
			||||||
 | 
					                file: file.clone(),
 | 
				
			||||||
 | 
					                mime: Some(mime::TEXT_HTML),
 | 
				
			||||||
 | 
					                status: StatusCode::OK,
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Directory listing
 | 
				
			||||||
 | 
					        let path_as_dir: Cow<str> = if path.is_empty() || path.ends_with('/') {
 | 
				
			||||||
 | 
					            path.into()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            format!("{path}/").into()
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        if self
 | 
				
			||||||
 | 
					            .files
 | 
				
			||||||
 | 
					            .keys()
 | 
				
			||||||
 | 
					            .any(|n| n.starts_with(path_as_dir.as_ref()))
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            let mut rev = false;
 | 
				
			||||||
 | 
					            let mut col = b'N';
 | 
				
			||||||
 | 
					            for (k, v) in url::form_urlencoded::parse(url_query.as_bytes()) {
 | 
				
			||||||
 | 
					                if k == "C" && !v.is_empty() {
 | 
				
			||||||
 | 
					                    col = v.as_bytes()[0];
 | 
				
			||||||
 | 
					                } else if k == "O" {
 | 
				
			||||||
 | 
					                    rev = v == "D";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return Ok(GetFileResult::Listing(self.get_listing(
 | 
				
			||||||
 | 
					                &path_as_dir,
 | 
				
			||||||
 | 
					                col,
 | 
				
			||||||
 | 
					                rev,
 | 
				
			||||||
 | 
					            )));
 | 
				
			||||||
 | 
					        } else if let Some(file) = self.files.get("404.html") {
 | 
				
			||||||
 | 
					            // Custom 404 error page
 | 
				
			||||||
 | 
					            return Ok(GetFileResult::File(GetFileResultFile {
 | 
				
			||||||
 | 
					                file: file.clone(),
 | 
				
			||||||
 | 
					                mime: Some(mime::TEXT_HTML),
 | 
				
			||||||
 | 
					                status: StatusCode::NOT_FOUND,
 | 
				
			||||||
 | 
					            }));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Err(Error::NotFound("requested file".into()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn get_files(&self) -> Vec<IndexEntry> {
 | 
				
			||||||
 | 
					        self.files
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .map(|(n, entry)| IndexEntry {
 | 
				
			||||||
 | 
					                name: n.to_owned(),
 | 
				
			||||||
 | 
					                size: entry.uncompressed_size,
 | 
				
			||||||
 | 
					                crc32: entry.crc32,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .collect()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_listing(&self, path: &str, col: u8, rev: bool) -> Listing {
 | 
				
			||||||
 | 
					        let entries = self
 | 
				
			||||||
 | 
					            .files
 | 
				
			||||||
 | 
					            .iter()
 | 
				
			||||||
 | 
					            .filter_map(|(n, entry)| {
 | 
				
			||||||
 | 
					                n.strip_prefix(path).map(|n| {
 | 
				
			||||||
 | 
					                    let n = n.split_inclusive('/').next().unwrap();
 | 
				
			||||||
 | 
					                    (n, entry)
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .collect::<BTreeMap<_, _>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Put directories first
 | 
				
			||||||
 | 
					        let mut directories = Vec::new();
 | 
				
			||||||
 | 
					        let mut files = Vec::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let entries_iter: Box<dyn Iterator<Item = (&str, &FileEntry)>> = if col == b'N' && rev {
 | 
				
			||||||
 | 
					            Box::new(entries.into_iter().rev())
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Box::new(entries.into_iter())
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (n, entry) in entries_iter {
 | 
				
			||||||
 | 
					            if n.ends_with('/') {
 | 
				
			||||||
 | 
					                directories.push(ListingEntry {
 | 
				
			||||||
 | 
					                    name: n.to_owned(),
 | 
				
			||||||
 | 
					                    url: format!("{n}{path}"),
 | 
				
			||||||
 | 
					                    size: 0,
 | 
				
			||||||
 | 
					                    crc32: "-".to_string(),
 | 
				
			||||||
 | 
					                    is_dir: true,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                files.push(ListingEntry {
 | 
				
			||||||
 | 
					                    name: n.to_owned(),
 | 
				
			||||||
 | 
					                    url: format!("{n}{path}"),
 | 
				
			||||||
 | 
					                    size: entry.uncompressed_size,
 | 
				
			||||||
 | 
					                    crc32: hex::encode(entry.crc32.to_le_bytes()),
 | 
				
			||||||
 | 
					                    is_dir: false,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Sort by size
 | 
				
			||||||
 | 
					        if col == b'S' {
 | 
				
			||||||
 | 
					            if rev {
 | 
				
			||||||
 | 
					                files.sort_by(|a, b| b.size.cmp(&a.size));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                files.sort_by_key(|f| f.size);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let n_dirs = directories.len();
 | 
				
			||||||
 | 
					        let n_files = files.len();
 | 
				
			||||||
 | 
					        directories.append(&mut files);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Listing {
 | 
				
			||||||
 | 
					            entries: directories,
 | 
				
			||||||
 | 
					            n_dirs,
 | 
				
			||||||
 | 
					            n_files,
 | 
				
			||||||
 | 
					            has_parent: !path.is_empty(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue