├── .clippy.toml ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── extract_file.rs ├── list.rs ├── raw_list.rs └── write.rs ├── src ├── archive.rs ├── builder.rs ├── entry.rs ├── entry_type.rs ├── error.rs ├── header.rs ├── lib.rs └── pax.rs └── tests ├── all.rs ├── archives ├── 7z_long_path.tar ├── directory.tar ├── duplicate_dirs.tar ├── empty_filename.tar ├── file_times.tar ├── link.tar ├── pax.tar ├── pax2.tar ├── reading_files.tar ├── simple.tar ├── simple_missing_last_header.tar ├── spaces.tar ├── sparse.tar └── xattrs.tar ├── entry.rs └── header └── mod.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.51" 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | 7 | jobs: 8 | 9 | check: 10 | name: Test 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | build: [msrv, stable, beta, nightly, macos, windows] 15 | include: 16 | - build: msrv 17 | os: ubuntu-latest 18 | rust: 1.63 19 | - build: stable 20 | os: ubuntu-latest 21 | rust: stable 22 | - build: beta 23 | os: ubuntu-latest 24 | rust: beta 25 | - build: nightly 26 | os: ubuntu-latest 27 | rust: nightly 28 | - build: macos 29 | os: macos-latest 30 | rust: stable 31 | - build: windows 32 | os: windows-latest 33 | rust: stable 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions-rs/toolchain@v1 37 | with: 38 | profile: minimal 39 | override: true 40 | toolchain: ${{ matrix.rust }} 41 | - uses: actions-rs/cargo@v1 42 | with: 43 | command: test 44 | 45 | fmt: 46 | name: Check formatting 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | components: rustfmt 55 | override: true 56 | - uses: actions-rs/cargo@v1 57 | with: 58 | command: fmt 59 | args: --all -- --check 60 | 61 | lint: 62 | name: Clippy Linting 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: actions-rs/toolchain@v1 67 | with: 68 | profile: minimal 69 | toolchain: stable 70 | components: clippy 71 | override: true 72 | - uses: actions-rs/cargo@v1 73 | with: 74 | command: clippy 75 | args: --all 76 | 77 | docs: 78 | name: Check docs 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v4 82 | - uses: actions-rs/toolchain@v1 83 | with: 84 | profile: minimal 85 | toolchain: nightly 86 | - uses: actions-rs/cargo@v1 87 | with: 88 | command: doc 89 | args: --no-deps 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_field_init_shorthand = true -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-tar" 3 | version = "0.5.0" 4 | authors = ["dignifiedquire ", "Alex Crichton "] 5 | homepage = "https://github.com/dignifiedquire/async-tar" 6 | repository = "https://github.com/dignifiedquire/async-tar" 7 | documentation = "https://docs.rs/async-tar" 8 | license = "MIT/Apache-2.0" 9 | keywords = ["tar", "tarfile", "encoding"] 10 | readme = "README.md" 11 | edition = "2018" 12 | exclude = ["tests/archives/*"] 13 | resolver = "2" 14 | 15 | description = """ 16 | A Rust implementation of an async TAR file reader and writer. This library does not 17 | currently handle compression, but it is abstract over all I/O readers and 18 | writers. Additionally, great lengths are taken to ensure that the entire 19 | contents are never required to be entirely resident in memory all at once. 20 | """ 21 | 22 | [dependencies] 23 | async-std = { version = "1.12.0", features = ["unstable"] } 24 | filetime = "0.2.8" 25 | pin-project = "1.0.8" 26 | 27 | [dev-dependencies] 28 | async-std = { version = "1.12.0", features = ["unstable", "attributes"] } 29 | static_assertions = "1.1.0" 30 | tempfile = "3" 31 | 32 | [target."cfg(unix)".dependencies] 33 | libc = "0.2" 34 | xattr = { version = "0.2", optional = true } 35 | 36 | [target.'cfg(target_os = "redox")'.dependencies] 37 | redox_syscall = "0.2" 38 | 39 | [features] 40 | default = [ "xattr" ] 41 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Alex Crichton 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

async-tar

2 |
3 | 4 | A tar archive reading/writing library for async Rust. 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | Crates.io version 15 | 16 | 17 | 18 | Download 20 | 21 | 22 | 23 | docs.rs docs 25 | 26 |
27 | 28 |
29 |

30 | 31 | API Docs 32 | 33 | | 34 | 35 | Releases 36 | 37 |

38 |
39 |
40 | 41 | > Based on the great [tar-rs](https://github.com/alexcrichton/tar-rs). 42 | 43 | ## Reading an archive 44 | 45 | ```rust,no_run 46 | use async_std::io::stdin; 47 | use async_std::prelude::*; 48 | 49 | use async_tar::Archive; 50 | 51 | fn main() { 52 | async_std::task::block_on(async { 53 | let mut ar = Archive::new(stdin()); 54 | let mut entries = ar.entries().unwrap(); 55 | while let Some(file) = entries.next().await { 56 | let f = file.unwrap(); 57 | println!("{}", f.path().unwrap().display()); 58 | } 59 | }); 60 | } 61 | ``` 62 | 63 | ## Writing an archive 64 | 65 | ```rust,no_run 66 | use async_std::fs::File; 67 | use async_tar::Builder; 68 | 69 | fn main() { 70 | async_std::task::block_on(async { 71 | let file = File::create("foo.tar").await.unwrap(); 72 | let mut a = Builder::new(file); 73 | 74 | a.append_path("README.md").await.unwrap(); 75 | a.append_file("lib.rs", &mut File::open("src/lib.rs").await.unwrap()) 76 | .await 77 | .unwrap(); 78 | }); 79 | } 80 | ``` 81 | 82 | # MSRV 83 | 84 | Minimal stable rust version: 1.63 85 | 86 | *An increase to the MSRV is accompanied by a minor version bump* 87 | 88 | # License 89 | 90 | This project is licensed under either of 91 | 92 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 93 | http://www.apache.org/licenses/LICENSE-2.0) 94 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 95 | http://opensource.org/licenses/MIT) 96 | 97 | at your option. 98 | 99 | ### Contribution 100 | 101 | Unless you explicitly state otherwise, any contribution intentionally submitted 102 | for inclusion in this project by you, as defined in the Apache-2.0 license, 103 | shall be dual licensed as above, without any additional terms or conditions. 104 | -------------------------------------------------------------------------------- /examples/extract_file.rs: -------------------------------------------------------------------------------- 1 | //! An example of extracting a file in an archive. 2 | //! 3 | //! Takes a tarball on standard input, looks for an entry with a listed file 4 | //! name as the first argument provided, and then prints the contents of that 5 | //! file to stdout. 6 | 7 | extern crate async_tar; 8 | 9 | use async_std::{ 10 | io::{copy, stdin, stdout}, 11 | path::Path, 12 | prelude::*, 13 | }; 14 | use std::env::args_os; 15 | 16 | use async_tar::Archive; 17 | 18 | fn main() { 19 | async_std::task::block_on(async { 20 | let first_arg = args_os().nth(1).unwrap(); 21 | let filename = Path::new(&first_arg); 22 | let ar = Archive::new(stdin()); 23 | let mut entries = ar.entries().unwrap(); 24 | while let Some(file) = entries.next().await { 25 | let mut f = file.unwrap(); 26 | if f.path().unwrap() == filename { 27 | copy(&mut f, &mut stdout()).await.unwrap(); 28 | } 29 | } 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /examples/list.rs: -------------------------------------------------------------------------------- 1 | //! An example of listing the file names of entries in an archive. 2 | //! 3 | //! Takes a tarball on stdin and prints out all of the entries inside. 4 | 5 | extern crate async_tar; 6 | 7 | use async_std::{io::stdin, prelude::*}; 8 | 9 | use async_tar::Archive; 10 | 11 | fn main() { 12 | async_std::task::block_on(async { 13 | let ar = Archive::new(stdin()); 14 | let mut entries = ar.entries().unwrap(); 15 | while let Some(file) = entries.next().await { 16 | let f = file.unwrap(); 17 | println!("{}", f.path().unwrap().display()); 18 | } 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /examples/raw_list.rs: -------------------------------------------------------------------------------- 1 | //! An example of listing raw entries in an archive. 2 | //! 3 | //! Takes a tarball on stdin and prints out all of the entries inside. 4 | 5 | extern crate async_tar; 6 | 7 | use async_std::{io::stdin, prelude::*}; 8 | 9 | use async_tar::Archive; 10 | 11 | fn main() { 12 | async_std::task::block_on(async { 13 | let ar = Archive::new(stdin()); 14 | let mut i = 0; 15 | let mut entries = ar.entries_raw().unwrap(); 16 | while let Some(file) = entries.next().await { 17 | println!("-------------------------- Entry {}", i); 18 | let mut f = file.unwrap(); 19 | println!("path: {}", f.path().unwrap().display()); 20 | println!("size: {}", f.header().size().unwrap()); 21 | println!("entry size: {}", f.header().entry_size().unwrap()); 22 | println!("link name: {:?}", f.link_name().unwrap()); 23 | println!("file type: {:#x}", f.header().entry_type().as_byte()); 24 | println!("mode: {:#o}", f.header().mode().unwrap()); 25 | println!("uid: {}", f.header().uid().unwrap()); 26 | println!("gid: {}", f.header().gid().unwrap()); 27 | println!("mtime: {}", f.header().mtime().unwrap()); 28 | println!("username: {:?}", f.header().username().unwrap()); 29 | println!("groupname: {:?}", f.header().groupname().unwrap()); 30 | 31 | if f.header().as_ustar().is_some() { 32 | println!("kind: UStar"); 33 | } else if f.header().as_gnu().is_some() { 34 | println!("kind: GNU"); 35 | } else { 36 | println!("kind: normal"); 37 | } 38 | 39 | if let Ok(Some(extensions)) = f.pax_extensions().await { 40 | println!("pax extensions:"); 41 | for e in extensions { 42 | let e = e.unwrap(); 43 | println!( 44 | "\t{:?} = {:?}", 45 | String::from_utf8_lossy(e.key_bytes()), 46 | String::from_utf8_lossy(e.value_bytes()) 47 | ); 48 | } 49 | } 50 | i += 1; 51 | } 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /examples/write.rs: -------------------------------------------------------------------------------- 1 | extern crate async_tar; 2 | 3 | use async_std::fs::File; 4 | use async_tar::Builder; 5 | 6 | fn main() { 7 | async_std::task::block_on(async { 8 | let file = File::create("foo.tar").await.unwrap(); 9 | let mut a = Builder::new(file); 10 | 11 | a.append_path("README.md").await.unwrap(); 12 | a.append_file("lib.rs", &mut File::open("src/lib.rs").await.unwrap()) 13 | .await 14 | .unwrap(); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/archive.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp, 3 | pin::Pin, 4 | sync::{Arc, Mutex}, 5 | }; 6 | 7 | use async_std::{ 8 | fs, io, 9 | io::prelude::*, 10 | path::Path, 11 | prelude::*, 12 | stream::Stream, 13 | task::{Context, Poll}, 14 | }; 15 | use pin_project::pin_project; 16 | 17 | use crate::{ 18 | entry::{EntryFields, EntryIo}, 19 | error::TarError, 20 | other, Entry, GnuExtSparseHeader, GnuSparseHeader, Header, 21 | }; 22 | 23 | /// A top-level representation of an archive file. 24 | /// 25 | /// This archive can have an entry added to it and it can be iterated over. 26 | #[derive(Debug)] 27 | pub struct Archive { 28 | inner: Arc>>, 29 | } 30 | 31 | impl Clone for Archive { 32 | fn clone(&self) -> Self { 33 | Archive { 34 | inner: self.inner.clone(), 35 | } 36 | } 37 | } 38 | 39 | #[pin_project] 40 | #[derive(Debug)] 41 | pub struct ArchiveInner { 42 | pos: u64, 43 | unpack_xattrs: bool, 44 | preserve_permissions: bool, 45 | preserve_mtime: bool, 46 | ignore_zeros: bool, 47 | #[pin] 48 | obj: R, 49 | } 50 | 51 | /// Configure the archive. 52 | pub struct ArchiveBuilder { 53 | obj: R, 54 | unpack_xattrs: bool, 55 | preserve_permissions: bool, 56 | preserve_mtime: bool, 57 | ignore_zeros: bool, 58 | } 59 | 60 | impl ArchiveBuilder { 61 | /// Create a new builder. 62 | pub fn new(obj: R) -> Self { 63 | ArchiveBuilder { 64 | unpack_xattrs: false, 65 | preserve_permissions: false, 66 | preserve_mtime: true, 67 | ignore_zeros: false, 68 | obj, 69 | } 70 | } 71 | 72 | /// Indicate whether extended file attributes (xattrs on Unix) are preserved 73 | /// when unpacking this archive. 74 | /// 75 | /// This flag is disabled by default and is currently only implemented on 76 | /// Unix using xattr support. This may eventually be implemented for 77 | /// Windows, however, if other archive implementations are found which do 78 | /// this as well. 79 | pub fn set_unpack_xattrs(mut self, unpack_xattrs: bool) -> Self { 80 | self.unpack_xattrs = unpack_xattrs; 81 | self 82 | } 83 | 84 | /// Indicate whether extended permissions (like suid on Unix) are preserved 85 | /// when unpacking this entry. 86 | /// 87 | /// This flag is disabled by default and is currently only implemented on 88 | /// Unix. 89 | pub fn set_preserve_permissions(mut self, preserve: bool) -> Self { 90 | self.preserve_permissions = preserve; 91 | self 92 | } 93 | 94 | /// Indicate whether access time information is preserved when unpacking 95 | /// this entry. 96 | /// 97 | /// This flag is enabled by default. 98 | pub fn set_preserve_mtime(mut self, preserve: bool) -> Self { 99 | self.preserve_mtime = preserve; 100 | self 101 | } 102 | 103 | /// Ignore zeroed headers, which would otherwise indicate to the archive that it has no more 104 | /// entries. 105 | /// 106 | /// This can be used in case multiple tar archives have been concatenated together. 107 | pub fn set_ignore_zeros(mut self, ignore_zeros: bool) -> Self { 108 | self.ignore_zeros = ignore_zeros; 109 | self 110 | } 111 | 112 | /// Construct the archive, ready to accept inputs. 113 | pub fn build(self) -> Archive { 114 | let Self { 115 | unpack_xattrs, 116 | preserve_permissions, 117 | preserve_mtime, 118 | ignore_zeros, 119 | obj, 120 | } = self; 121 | 122 | Archive { 123 | inner: Arc::new(Mutex::new(ArchiveInner { 124 | unpack_xattrs, 125 | preserve_permissions, 126 | preserve_mtime, 127 | ignore_zeros, 128 | obj, 129 | pos: 0, 130 | })), 131 | } 132 | } 133 | } 134 | 135 | impl Archive { 136 | /// Create a new archive with the underlying object as the reader. 137 | pub fn new(obj: R) -> Archive { 138 | Archive { 139 | inner: Arc::new(Mutex::new(ArchiveInner { 140 | unpack_xattrs: false, 141 | preserve_permissions: false, 142 | preserve_mtime: true, 143 | ignore_zeros: false, 144 | obj, 145 | pos: 0, 146 | })), 147 | } 148 | } 149 | 150 | /// Unwrap this archive, returning the underlying object. 151 | pub fn into_inner(self) -> Result { 152 | match Arc::try_unwrap(self.inner) { 153 | Ok(inner) => Ok(inner.into_inner().unwrap().obj), 154 | Err(inner) => Err(Self { inner }), 155 | } 156 | } 157 | 158 | /// Construct an stream over the entries in this archive. 159 | /// 160 | /// Note that care must be taken to consider each entry within an archive in 161 | /// sequence. If entries are processed out of sequence (from what the 162 | /// stream returns), then the contents read for each entry may be 163 | /// corrupted. 164 | pub fn entries(self) -> io::Result> { 165 | if self.inner.lock().unwrap().pos != 0 { 166 | return Err(other( 167 | "cannot call entries unless archive is at \ 168 | position 0", 169 | )); 170 | } 171 | 172 | Ok(Entries { 173 | archive: self, 174 | current: (0, None, 0, None), 175 | fields: None, 176 | gnu_longlink: None, 177 | gnu_longname: None, 178 | pax_extensions: None, 179 | }) 180 | } 181 | 182 | /// Construct an stream over the raw entries in this archive. 183 | /// 184 | /// Note that care must be taken to consider each entry within an archive in 185 | /// sequence. If entries are processed out of sequence (from what the 186 | /// stream returns), then the contents read for each entry may be 187 | /// corrupted. 188 | pub fn entries_raw(self) -> io::Result> { 189 | if self.inner.lock().unwrap().pos != 0 { 190 | return Err(other( 191 | "cannot call entries_raw unless archive is at \ 192 | position 0", 193 | )); 194 | } 195 | 196 | Ok(RawEntries { 197 | archive: self, 198 | current: (0, None, 0), 199 | }) 200 | } 201 | 202 | /// Unpacks the contents tarball into the specified `dst`. 203 | /// 204 | /// This function will iterate over the entire contents of this tarball, 205 | /// extracting each file in turn to the location specified by the entry's 206 | /// path name. 207 | /// 208 | /// This operation is relatively sensitive in that it will not write files 209 | /// outside of the path specified by `dst`. Files in the archive which have 210 | /// a '..' in their path are skipped during the unpacking process. 211 | /// 212 | /// # Examples 213 | /// 214 | /// ```no_run 215 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 216 | /// # 217 | /// use async_std::fs::File; 218 | /// use async_tar::Archive; 219 | /// 220 | /// let mut ar = Archive::new(File::open("foo.tar").await?); 221 | /// ar.unpack("foo").await?; 222 | /// # 223 | /// # Ok(()) }) } 224 | /// ``` 225 | pub async fn unpack>(self, dst: P) -> io::Result<()> { 226 | let mut entries = self.entries()?; 227 | let mut pinned = Pin::new(&mut entries); 228 | let dst = dst.as_ref(); 229 | 230 | if dst.symlink_metadata().await.is_err() { 231 | fs::create_dir_all(&dst) 232 | .await 233 | .map_err(|e| TarError::new(&format!("failed to create `{}`", dst.display()), e))?; 234 | } 235 | 236 | // Canonicalizing the dst directory will prepend the path with '\\?\' 237 | // on windows which will allow windows APIs to treat the path as an 238 | // extended-length path with a 32,767 character limit. Otherwise all 239 | // unpacked paths over 260 characters will fail on creation with a 240 | // NotFound exception. 241 | let dst = &dst 242 | .canonicalize() 243 | .await 244 | .unwrap_or_else(|_| dst.to_path_buf()); 245 | 246 | // Delay any directory entries until the end (they will be created if needed by 247 | // descendants), to ensure that directory permissions do not interfer with descendant 248 | // extraction. 249 | let mut directories = Vec::new(); 250 | while let Some(entry) = pinned.next().await { 251 | let mut file = entry.map_err(|e| TarError::new("failed to iterate over archive", e))?; 252 | if file.header().entry_type() == crate::EntryType::Directory { 253 | directories.push(file); 254 | } else { 255 | file.unpack_in(dst).await?; 256 | } 257 | } 258 | for mut dir in directories { 259 | dir.unpack_in(dst).await?; 260 | } 261 | 262 | Ok(()) 263 | } 264 | } 265 | 266 | /// Stream of `Entry`s. 267 | #[pin_project] 268 | #[derive(Debug)] 269 | pub struct Entries { 270 | archive: Archive, 271 | current: (u64, Option
, usize, Option), 272 | fields: Option>>, 273 | gnu_longname: Option>, 274 | gnu_longlink: Option>, 275 | pax_extensions: Option>, 276 | } 277 | 278 | macro_rules! ready_opt_err { 279 | ($val:expr) => { 280 | match async_std::task::ready!($val) { 281 | Some(Ok(val)) => val, 282 | Some(Err(err)) => return Poll::Ready(Some(Err(err))), 283 | None => return Poll::Ready(None), 284 | } 285 | }; 286 | } 287 | 288 | macro_rules! ready_err { 289 | ($val:expr) => { 290 | match async_std::task::ready!($val) { 291 | Ok(val) => val, 292 | Err(err) => return Poll::Ready(Some(Err(err))), 293 | } 294 | }; 295 | } 296 | 297 | impl Stream for Entries { 298 | type Item = io::Result>>; 299 | 300 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 301 | let mut this = self.project(); 302 | loop { 303 | let (next, current_header, current_header_pos, _) = &mut this.current; 304 | 305 | let fields = if let Some(fields) = this.fields.as_mut() { 306 | fields 307 | } else { 308 | *this.fields = Some(EntryFields::from(ready_opt_err!(poll_next_raw( 309 | this.archive, 310 | next, 311 | current_header, 312 | current_header_pos, 313 | cx 314 | )))); 315 | continue; 316 | }; 317 | 318 | let is_recognized_header = 319 | fields.header.as_gnu().is_some() || fields.header.as_ustar().is_some(); 320 | if is_recognized_header && fields.header.entry_type().is_gnu_longname() { 321 | if this.gnu_longname.is_some() { 322 | return Poll::Ready(Some(Err(other( 323 | "two long name entries describing \ 324 | the same member", 325 | )))); 326 | } 327 | 328 | *this.gnu_longname = Some(ready_err!(Pin::new(fields).poll_read_all(cx))); 329 | *this.fields = None; 330 | continue; 331 | } 332 | 333 | if is_recognized_header && fields.header.entry_type().is_gnu_longlink() { 334 | if this.gnu_longlink.is_some() { 335 | return Poll::Ready(Some(Err(other( 336 | "two long name entries describing \ 337 | the same member", 338 | )))); 339 | } 340 | *this.gnu_longlink = Some(ready_err!(Pin::new(fields).poll_read_all(cx))); 341 | *this.fields = None; 342 | continue; 343 | } 344 | 345 | if is_recognized_header && fields.header.entry_type().is_pax_local_extensions() { 346 | if this.pax_extensions.is_some() { 347 | return Poll::Ready(Some(Err(other( 348 | "two pax extensions entries describing \ 349 | the same member", 350 | )))); 351 | } 352 | *this.pax_extensions = Some(ready_err!(Pin::new(fields).poll_read_all(cx))); 353 | *this.fields = None; 354 | continue; 355 | } 356 | 357 | fields.long_pathname = this.gnu_longname.take(); 358 | fields.long_linkname = this.gnu_longlink.take(); 359 | fields.pax_extensions = this.pax_extensions.take(); 360 | 361 | let (next, _, current_pos, current_ext) = &mut this.current; 362 | ready_err!(poll_parse_sparse_header( 363 | this.archive, 364 | next, 365 | current_ext, 366 | current_pos, 367 | fields, 368 | cx 369 | )); 370 | 371 | return Poll::Ready(Some(Ok(this.fields.take().unwrap().into_entry()))); 372 | } 373 | } 374 | } 375 | 376 | /// Stream of raw `Entry`s. 377 | pub struct RawEntries { 378 | archive: Archive, 379 | current: (u64, Option
, usize), 380 | } 381 | 382 | impl Stream for RawEntries { 383 | type Item = io::Result>>; 384 | 385 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 386 | let archive = self.archive.clone(); 387 | let (next, current_header, current_header_pos) = &mut self.current; 388 | poll_next_raw(&archive, next, current_header, current_header_pos, cx) 389 | } 390 | } 391 | 392 | fn poll_next_raw( 393 | archive: &Archive, 394 | next: &mut u64, 395 | current_header: &mut Option
, 396 | current_header_pos: &mut usize, 397 | cx: &mut Context<'_>, 398 | ) -> Poll>>>> { 399 | let mut header_pos = *next; 400 | 401 | loop { 402 | let archive = archive.clone(); 403 | // Seek to the start of the next header in the archive 404 | if current_header.is_none() { 405 | let delta = *next - archive.inner.lock().unwrap().pos; 406 | match async_std::task::ready!(poll_skip(archive.clone(), cx, delta)) { 407 | Ok(_) => {} 408 | Err(err) => return Poll::Ready(Some(Err(err))), 409 | } 410 | 411 | *current_header = Some(Header::new_old()); 412 | *current_header_pos = 0; 413 | } 414 | 415 | let header = current_header.as_mut().unwrap(); 416 | 417 | // EOF is an indicator that we are at the end of the archive. 418 | match async_std::task::ready!(poll_try_read_all( 419 | archive.clone(), 420 | cx, 421 | header.as_mut_bytes(), 422 | current_header_pos, 423 | )) { 424 | Ok(true) => {} 425 | Ok(false) => return Poll::Ready(None), 426 | Err(err) => return Poll::Ready(Some(Err(err))), 427 | } 428 | 429 | // If a header is not all zeros, we have another valid header. 430 | // Otherwise, check if we are ignoring zeros and continue, or break as if this is the 431 | // end of the archive. 432 | if !header.as_bytes().iter().all(|i| *i == 0) { 433 | *next += 512; 434 | break; 435 | } 436 | 437 | if !archive.inner.lock().unwrap().ignore_zeros { 438 | return Poll::Ready(None); 439 | } 440 | 441 | *next += 512; 442 | header_pos = *next; 443 | } 444 | 445 | let header = current_header.as_mut().unwrap(); 446 | 447 | // Make sure the checksum is ok 448 | let sum = header.as_bytes()[..148] 449 | .iter() 450 | .chain(&header.as_bytes()[156..]) 451 | .fold(0, |a, b| a + (*b as u32)) 452 | + 8 * 32; 453 | let cksum = header.cksum()?; 454 | if sum != cksum { 455 | return Poll::Ready(Some(Err(other("archive header checksum mismatch")))); 456 | } 457 | 458 | let file_pos = *next; 459 | let size = header.entry_size()?; 460 | 461 | let data = EntryIo::Data(archive.clone().take(size)); 462 | 463 | let header = current_header.take().unwrap(); 464 | 465 | let ArchiveInner { 466 | unpack_xattrs, 467 | preserve_mtime, 468 | preserve_permissions, 469 | .. 470 | } = &*archive.inner.lock().unwrap(); 471 | 472 | let ret = EntryFields { 473 | size, 474 | header_pos, 475 | file_pos, 476 | data: vec![data], 477 | header, 478 | long_pathname: None, 479 | long_linkname: None, 480 | pax_extensions: None, 481 | unpack_xattrs: *unpack_xattrs, 482 | preserve_permissions: *preserve_permissions, 483 | preserve_mtime: *preserve_mtime, 484 | read_state: None, 485 | }; 486 | 487 | // Store where the next entry is, rounding up by 512 bytes (the size of 488 | // a header); 489 | let size = (size + 511) & !(512 - 1); 490 | *next += size; 491 | 492 | Poll::Ready(Some(Ok(ret.into_entry()))) 493 | } 494 | 495 | fn poll_parse_sparse_header( 496 | archive: &Archive, 497 | next: &mut u64, 498 | current_ext: &mut Option, 499 | current_ext_pos: &mut usize, 500 | entry: &mut EntryFields>, 501 | cx: &mut Context<'_>, 502 | ) -> Poll> { 503 | if !entry.header.entry_type().is_gnu_sparse() { 504 | return Poll::Ready(Ok(())); 505 | } 506 | 507 | let gnu = match entry.header.as_gnu() { 508 | Some(gnu) => gnu, 509 | None => return Poll::Ready(Err(other("sparse entry type listed but not GNU header"))), 510 | }; 511 | 512 | // Sparse files are represented internally as a list of blocks that are 513 | // read. Blocks are either a bunch of 0's or they're data from the 514 | // underlying archive. 515 | // 516 | // Blocks of a sparse file are described by the `GnuSparseHeader` 517 | // structure, some of which are contained in `GnuHeader` but some of 518 | // which may also be contained after the first header in further 519 | // headers. 520 | // 521 | // We read off all the blocks here and use the `add_block` function to 522 | // incrementally add them to the list of I/O block (in `entry.data`). 523 | // The `add_block` function also validates that each chunk comes after 524 | // the previous, we don't overrun the end of the file, and each block is 525 | // aligned to a 512-byte boundary in the archive itself. 526 | // 527 | // At the end we verify that the sparse file size (`Header::size`) is 528 | // the same as the current offset (described by the list of blocks) as 529 | // well as the amount of data read equals the size of the entry 530 | // (`Header::entry_size`). 531 | entry.data.truncate(0); 532 | 533 | let mut cur = 0; 534 | let mut remaining = entry.size; 535 | { 536 | let data = &mut entry.data; 537 | let reader = archive.clone(); 538 | let size = entry.size; 539 | let mut add_block = |block: &GnuSparseHeader| -> io::Result<_> { 540 | if block.is_empty() { 541 | return Ok(()); 542 | } 543 | let off = block.offset()?; 544 | let len = block.length()?; 545 | 546 | if (size - remaining) % 512 != 0 { 547 | return Err(other( 548 | "previous block in sparse file was not \ 549 | aligned to 512-byte boundary", 550 | )); 551 | } else if off < cur { 552 | return Err(other( 553 | "out of order or overlapping sparse \ 554 | blocks", 555 | )); 556 | } else if cur < off { 557 | let block = io::repeat(0).take(off - cur); 558 | data.push(EntryIo::Pad(block)); 559 | } 560 | cur = off 561 | .checked_add(len) 562 | .ok_or_else(|| other("more bytes listed in sparse file than u64 can hold"))?; 563 | remaining = remaining.checked_sub(len).ok_or_else(|| { 564 | other( 565 | "sparse file consumed more data than the header \ 566 | listed", 567 | ) 568 | })?; 569 | data.push(EntryIo::Data(reader.clone().take(len))); 570 | Ok(()) 571 | }; 572 | for block in &gnu.sparse { 573 | add_block(block)? 574 | } 575 | if gnu.is_extended() { 576 | let started_header = current_ext.is_some(); 577 | if !started_header { 578 | let mut ext = GnuExtSparseHeader::new(); 579 | ext.isextended[0] = 1; 580 | *current_ext = Some(ext); 581 | *current_ext_pos = 0; 582 | } 583 | 584 | let ext = current_ext.as_mut().unwrap(); 585 | while ext.is_extended() { 586 | match async_std::task::ready!(poll_try_read_all( 587 | archive.clone(), 588 | cx, 589 | ext.as_mut_bytes(), 590 | current_ext_pos, 591 | )) { 592 | Ok(true) => {} 593 | Ok(false) => return Poll::Ready(Err(other("failed to read extension"))), 594 | Err(err) => return Poll::Ready(Err(err)), 595 | } 596 | 597 | *next += 512; 598 | for block in &ext.sparse { 599 | add_block(block)?; 600 | } 601 | } 602 | } 603 | } 604 | if cur != gnu.real_size()? { 605 | return Poll::Ready(Err(other( 606 | "mismatch in sparse file chunks and \ 607 | size in header", 608 | ))); 609 | } 610 | entry.size = cur; 611 | if remaining > 0 { 612 | return Poll::Ready(Err(other( 613 | "mismatch in sparse file chunks and \ 614 | entry size in header", 615 | ))); 616 | } 617 | 618 | Poll::Ready(Ok(())) 619 | } 620 | 621 | impl Read for Archive { 622 | fn poll_read( 623 | self: Pin<&mut Self>, 624 | cx: &mut Context<'_>, 625 | into: &mut [u8], 626 | ) -> Poll> { 627 | let mut lock = self.inner.lock().unwrap(); 628 | let mut inner = Pin::new(&mut *lock); 629 | let r = Pin::new(&mut inner.obj); 630 | 631 | let res = async_std::task::ready!(r.poll_read(cx, into)); 632 | match res { 633 | Ok(i) => { 634 | inner.pos += i as u64; 635 | Poll::Ready(Ok(i)) 636 | } 637 | Err(err) => Poll::Ready(Err(err)), 638 | } 639 | } 640 | } 641 | 642 | /// Try to fill the buffer from the reader. 643 | /// 644 | /// If the reader reaches its end before filling the buffer at all, returns `false`. 645 | /// Otherwise returns `true`. 646 | fn poll_try_read_all( 647 | mut source: R, 648 | cx: &mut Context<'_>, 649 | buf: &mut [u8], 650 | pos: &mut usize, 651 | ) -> Poll> { 652 | while *pos < buf.len() { 653 | match async_std::task::ready!(Pin::new(&mut source).poll_read(cx, &mut buf[*pos..])) { 654 | Ok(0) => { 655 | if *pos == 0 { 656 | return Poll::Ready(Ok(false)); 657 | } 658 | 659 | return Poll::Ready(Err(other("failed to read entire block"))); 660 | } 661 | Ok(n) => *pos += n, 662 | Err(err) => return Poll::Ready(Err(err)), 663 | } 664 | } 665 | 666 | *pos = 0; 667 | Poll::Ready(Ok(true)) 668 | } 669 | 670 | /// Skip n bytes on the given source. 671 | fn poll_skip( 672 | mut source: R, 673 | cx: &mut Context<'_>, 674 | mut amt: u64, 675 | ) -> Poll> { 676 | let mut buf = [0u8; 4096 * 8]; 677 | while amt > 0 { 678 | let n = cmp::min(amt, buf.len() as u64); 679 | match async_std::task::ready!(Pin::new(&mut source).poll_read(cx, &mut buf[..n as usize])) { 680 | Ok(0) => { 681 | return Poll::Ready(Err(other("unexpected EOF during skip"))); 682 | } 683 | Ok(n) => { 684 | amt -= n as u64; 685 | } 686 | Err(err) => return Poll::Ready(Err(err)), 687 | } 688 | } 689 | 690 | Poll::Ready(Ok(())) 691 | } 692 | 693 | #[cfg(test)] 694 | mod tests { 695 | use super::*; 696 | 697 | assert_impl_all!(async_std::fs::File: Send, Sync); 698 | assert_impl_all!(Entries: Send, Sync); 699 | assert_impl_all!(Archive: Send, Sync); 700 | assert_impl_all!(Entry>: Send, Sync); 701 | } 702 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use async_std::{ 4 | fs, 5 | io::{self, Read, Write}, 6 | path::Path, 7 | prelude::*, 8 | }; 9 | 10 | use crate::{ 11 | header::{bytes2path, path2bytes, HeaderMode}, 12 | other, EntryType, Header, 13 | }; 14 | 15 | /// A structure for building archives 16 | /// 17 | /// This structure has methods for building up an archive from scratch into any 18 | /// arbitrary writer. 19 | pub struct Builder { 20 | mode: HeaderMode, 21 | follow: bool, 22 | finished: bool, 23 | obj: Option, 24 | } 25 | 26 | impl Builder { 27 | /// Create a new archive builder with the underlying object as the 28 | /// destination of all data written. The builder will use 29 | /// `HeaderMode::Complete` by default. 30 | pub fn new(obj: W) -> Builder { 31 | Builder { 32 | mode: HeaderMode::Complete, 33 | follow: true, 34 | finished: false, 35 | obj: Some(obj), 36 | } 37 | } 38 | 39 | /// Changes the HeaderMode that will be used when reading fs Metadata for 40 | /// methods that implicitly read metadata for an input Path. Notably, this 41 | /// does _not_ apply to `append(Header)`. 42 | pub fn mode(&mut self, mode: HeaderMode) { 43 | self.mode = mode; 44 | } 45 | 46 | /// Follow symlinks, archiving the contents of the file they point to rather 47 | /// than adding a symlink to the archive. Defaults to true. 48 | pub fn follow_symlinks(&mut self, follow: bool) { 49 | self.follow = follow; 50 | } 51 | 52 | /// Gets shared reference to the underlying object. 53 | pub fn get_ref(&self) -> &W { 54 | self.obj.as_ref().unwrap() 55 | } 56 | 57 | /// Gets mutable reference to the underlying object. 58 | /// 59 | /// Note that care must be taken while writing to the underlying 60 | /// object. But, e.g. `get_mut().flush()` is claimed to be safe and 61 | /// useful in the situations when one needs to be ensured that 62 | /// tar entry was flushed to the disk. 63 | pub fn get_mut(&mut self) -> &mut W { 64 | self.obj.as_mut().unwrap() 65 | } 66 | 67 | /// Unwrap this archive, returning the underlying object. 68 | /// 69 | /// This function will finish writing the archive if the `finish` function 70 | /// hasn't yet been called, returning any I/O error which happens during 71 | /// that operation. 72 | pub async fn into_inner(mut self) -> io::Result { 73 | if !self.finished { 74 | self.finish().await?; 75 | } 76 | Ok(self.obj.take().unwrap()) 77 | } 78 | 79 | /// Adds a new entry to this archive. 80 | /// 81 | /// This function will append the header specified, followed by contents of 82 | /// the stream specified by `data`. To produce a valid archive the `size` 83 | /// field of `header` must be the same as the length of the stream that's 84 | /// being written. Additionally the checksum for the header should have been 85 | /// set via the `set_cksum` method. 86 | /// 87 | /// Note that this will not attempt to seek the archive to a valid position, 88 | /// so if the archive is in the middle of a read or some other similar 89 | /// operation then this may corrupt the archive. 90 | /// 91 | /// Also note that after all entries have been written to an archive the 92 | /// `finish` function needs to be called to finish writing the archive. 93 | /// 94 | /// # Errors 95 | /// 96 | /// This function will return an error for any intermittent I/O error which 97 | /// occurs when either reading or writing. 98 | /// 99 | /// # Examples 100 | /// 101 | /// ``` 102 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 103 | /// # 104 | /// use async_tar::{Builder, Header}; 105 | /// 106 | /// let mut header = Header::new_gnu(); 107 | /// header.set_path("foo")?; 108 | /// header.set_size(4); 109 | /// header.set_cksum(); 110 | /// 111 | /// let mut data: &[u8] = &[1, 2, 3, 4]; 112 | /// 113 | /// let mut ar = Builder::new(Vec::new()); 114 | /// ar.append(&header, data).await?; 115 | /// let data = ar.into_inner().await?; 116 | /// # 117 | /// # Ok(()) }) } 118 | /// ``` 119 | pub async fn append( 120 | &mut self, 121 | header: &Header, 122 | mut data: R, 123 | ) -> io::Result<()> { 124 | append(self.get_mut(), header, &mut data).await?; 125 | 126 | Ok(()) 127 | } 128 | 129 | /// Adds a new entry to this archive with the specified path. 130 | /// 131 | /// This function will set the specified path in the given header, which may 132 | /// require appending a GNU long-name extension entry to the archive first. 133 | /// The checksum for the header will be automatically updated via the 134 | /// `set_cksum` method after setting the path. No other metadata in the 135 | /// header will be modified. 136 | /// 137 | /// Then it will append the header, followed by contents of the stream 138 | /// specified by `data`. To produce a valid archive the `size` field of 139 | /// `header` must be the same as the length of the stream that's being 140 | /// written. 141 | /// 142 | /// Note that this will not attempt to seek the archive to a valid position, 143 | /// so if the archive is in the middle of a read or some other similar 144 | /// operation then this may corrupt the archive. 145 | /// 146 | /// Also note that after all entries have been written to an archive the 147 | /// `finish` function needs to be called to finish writing the archive. 148 | /// 149 | /// # Errors 150 | /// 151 | /// This function will return an error for any intermittent I/O error which 152 | /// occurs when either reading or writing. 153 | /// 154 | /// # Examples 155 | /// 156 | /// ``` 157 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 158 | /// # 159 | /// use async_tar::{Builder, Header}; 160 | /// 161 | /// let mut header = Header::new_gnu(); 162 | /// header.set_size(4); 163 | /// header.set_cksum(); 164 | /// 165 | /// let mut data: &[u8] = &[1, 2, 3, 4]; 166 | /// 167 | /// let mut ar = Builder::new(Vec::new()); 168 | /// ar.append_data(&mut header, "really/long/path/to/foo", data).await?; 169 | /// let data = ar.into_inner().await?; 170 | /// # 171 | /// # Ok(()) }) } 172 | /// ``` 173 | pub async fn append_data, R: Read + Unpin + Send>( 174 | &mut self, 175 | header: &mut Header, 176 | path: P, 177 | data: R, 178 | ) -> io::Result<()> { 179 | prepare_header_path(self.get_mut(), header, path.as_ref()).await?; 180 | header.set_cksum(); 181 | self.append(header, data).await?; 182 | 183 | Ok(()) 184 | } 185 | 186 | /// Adds a file on the local filesystem to this archive. 187 | /// 188 | /// This function will open the file specified by `path` and insert the file 189 | /// into the archive with the appropriate metadata set, returning any I/O 190 | /// error which occurs while writing. The path name for the file inside of 191 | /// this archive will be the same as `path`, and it is required that the 192 | /// path is a relative path. 193 | /// 194 | /// Note that this will not attempt to seek the archive to a valid position, 195 | /// so if the archive is in the middle of a read or some other similar 196 | /// operation then this may corrupt the archive. 197 | /// 198 | /// Also note that after all files have been written to an archive the 199 | /// `finish` function needs to be called to finish writing the archive. 200 | /// 201 | /// # Examples 202 | /// 203 | /// ```no_run 204 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 205 | /// # 206 | /// use async_tar::Builder; 207 | /// 208 | /// let mut ar = Builder::new(Vec::new()); 209 | /// 210 | /// ar.append_path("foo/bar.txt").await?; 211 | /// # 212 | /// # Ok(()) }) } 213 | /// ``` 214 | pub async fn append_path>(&mut self, path: P) -> io::Result<()> { 215 | let mode = self.mode; 216 | let follow = self.follow; 217 | append_path_with_name(self.get_mut(), path.as_ref(), None, mode, follow).await?; 218 | Ok(()) 219 | } 220 | 221 | /// Adds a file on the local filesystem to this archive under another name. 222 | /// 223 | /// This function will open the file specified by `path` and insert the file 224 | /// into the archive as `name` with appropriate metadata set, returning any 225 | /// I/O error which occurs while writing. The path name for the file inside 226 | /// of this archive will be `name` is required to be a relative path. 227 | /// 228 | /// Note that this will not attempt to seek the archive to a valid position, 229 | /// so if the archive is in the middle of a read or some other similar 230 | /// operation then this may corrupt the archive. 231 | /// 232 | /// Also note that after all files have been written to an archive the 233 | /// `finish` function needs to be called to finish writing the archive. 234 | /// 235 | /// # Examples 236 | /// 237 | /// ```no_run 238 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 239 | /// # 240 | /// use async_tar::Builder; 241 | /// 242 | /// let mut ar = Builder::new(Vec::new()); 243 | /// 244 | /// // Insert the local file "foo/bar.txt" in the archive but with the name 245 | /// // "bar/foo.txt". 246 | /// ar.append_path_with_name("foo/bar.txt", "bar/foo.txt").await?; 247 | /// # 248 | /// # Ok(()) }) } 249 | /// ``` 250 | pub async fn append_path_with_name, N: AsRef>( 251 | &mut self, 252 | path: P, 253 | name: N, 254 | ) -> io::Result<()> { 255 | let mode = self.mode; 256 | let follow = self.follow; 257 | append_path_with_name( 258 | self.get_mut(), 259 | path.as_ref(), 260 | Some(name.as_ref()), 261 | mode, 262 | follow, 263 | ) 264 | .await?; 265 | Ok(()) 266 | } 267 | 268 | /// Adds a file to this archive with the given path as the name of the file 269 | /// in the archive. 270 | /// 271 | /// This will use the metadata of `file` to populate a `Header`, and it will 272 | /// then append the file to the archive with the name `path`. 273 | /// 274 | /// Note that this will not attempt to seek the archive to a valid position, 275 | /// so if the archive is in the middle of a read or some other similar 276 | /// operation then this may corrupt the archive. 277 | /// 278 | /// Also note that after all files have been written to an archive the 279 | /// `finish` function needs to be called to finish writing the archive. 280 | /// 281 | /// # Examples 282 | /// 283 | /// ```no_run 284 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 285 | /// # 286 | /// use async_std::fs::File; 287 | /// use async_tar::Builder; 288 | /// 289 | /// let mut ar = Builder::new(Vec::new()); 290 | /// 291 | /// // Open the file at one location, but insert it into the archive with a 292 | /// // different name. 293 | /// let mut f = File::open("foo/bar/baz.txt").await?; 294 | /// ar.append_file("bar/baz.txt", &mut f).await?; 295 | /// # 296 | /// # Ok(()) }) } 297 | /// ``` 298 | pub async fn append_file>( 299 | &mut self, 300 | path: P, 301 | file: &mut fs::File, 302 | ) -> io::Result<()> { 303 | let mode = self.mode; 304 | append_file(self.get_mut(), path.as_ref(), file, mode).await?; 305 | Ok(()) 306 | } 307 | 308 | /// Adds a directory to this archive with the given path as the name of the 309 | /// directory in the archive. 310 | /// 311 | /// This will use `stat` to populate a `Header`, and it will then append the 312 | /// directory to the archive with the name `path`. 313 | /// 314 | /// Note that this will not attempt to seek the archive to a valid position, 315 | /// so if the archive is in the middle of a read or some other similar 316 | /// operation then this may corrupt the archive. 317 | /// 318 | /// Also note that after all files have been written to an archive the 319 | /// `finish` function needs to be called to finish writing the archive. 320 | /// 321 | /// # Examples 322 | /// 323 | /// ``` 324 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 325 | /// # 326 | /// use async_std::fs; 327 | /// use async_tar::Builder; 328 | /// 329 | /// let mut ar = Builder::new(Vec::new()); 330 | /// 331 | /// // Use the directory at one location, but insert it into the archive 332 | /// // with a different name. 333 | /// ar.append_dir("bardir", ".").await?; 334 | /// # 335 | /// # Ok(()) }) } 336 | /// ``` 337 | pub async fn append_dir(&mut self, path: P, src_path: Q) -> io::Result<()> 338 | where 339 | P: AsRef, 340 | Q: AsRef, 341 | { 342 | let mode = self.mode; 343 | append_dir(self.get_mut(), path.as_ref(), src_path.as_ref(), mode).await?; 344 | Ok(()) 345 | } 346 | 347 | /// Adds a directory and all of its contents (recursively) to this archive 348 | /// with the given path as the name of the directory in the archive. 349 | /// 350 | /// Note that this will not attempt to seek the archive to a valid position, 351 | /// so if the archive is in the middle of a read or some other similar 352 | /// operation then this may corrupt the archive. 353 | /// 354 | /// Also note that after all files have been written to an archive the 355 | /// `finish` function needs to be called to finish writing the archive. 356 | /// 357 | /// # Examples 358 | /// 359 | /// ``` 360 | /// # fn main() -> Result<(), Box> { async_std::task::block_on(async { 361 | /// # 362 | /// use async_std::fs; 363 | /// use async_tar::Builder; 364 | /// 365 | /// let mut ar = Builder::new(Vec::new()); 366 | /// 367 | /// // Use the directory at one location, but insert it into the archive 368 | /// // with a different name. 369 | /// ar.append_dir_all("bardir", ".").await?; 370 | /// # 371 | /// # Ok(()) }) } 372 | /// ``` 373 | pub async fn append_dir_all(&mut self, path: P, src_path: Q) -> io::Result<()> 374 | where 375 | P: AsRef, 376 | Q: AsRef, 377 | { 378 | let mode = self.mode; 379 | let follow = self.follow; 380 | append_dir_all( 381 | self.get_mut(), 382 | path.as_ref(), 383 | src_path.as_ref(), 384 | mode, 385 | follow, 386 | ) 387 | .await?; 388 | Ok(()) 389 | } 390 | 391 | /// Finish writing this archive, emitting the termination sections. 392 | /// 393 | /// This function should only be called when the archive has been written 394 | /// entirely and if an I/O error happens the underlying object still needs 395 | /// to be acquired. 396 | /// 397 | /// In most situations the `into_inner` method should be preferred. 398 | pub async fn finish(&mut self) -> io::Result<()> { 399 | if self.finished { 400 | return Ok(()); 401 | } 402 | self.finished = true; 403 | self.get_mut().write_all(&[0; 1024]).await?; 404 | Ok(()) 405 | } 406 | } 407 | 408 | async fn append( 409 | mut dst: &mut (dyn Write + Unpin + Send), 410 | header: &Header, 411 | mut data: &mut (dyn Read + Unpin + Send), 412 | ) -> io::Result<()> { 413 | dst.write_all(header.as_bytes()).await?; 414 | let len = io::copy(&mut data, &mut dst).await?; 415 | 416 | // Pad with zeros if necessary. 417 | let buf = [0; 512]; 418 | let remaining = 512 - (len % 512); 419 | if remaining < 512 { 420 | dst.write_all(&buf[..remaining as usize]).await?; 421 | } 422 | 423 | Ok(()) 424 | } 425 | 426 | async fn append_path_with_name( 427 | dst: &mut (dyn Write + Unpin + Sync + Send), 428 | path: &Path, 429 | name: Option<&Path>, 430 | mode: HeaderMode, 431 | follow: bool, 432 | ) -> io::Result<()> { 433 | let stat = if follow { 434 | fs::metadata(path).await.map_err(|err| { 435 | io::Error::new( 436 | err.kind(), 437 | format!("{} when getting metadata for {}", err, path.display()), 438 | ) 439 | })? 440 | } else { 441 | fs::symlink_metadata(path).await.map_err(|err| { 442 | io::Error::new( 443 | err.kind(), 444 | format!("{} when getting metadata for {}", err, path.display()), 445 | ) 446 | })? 447 | }; 448 | let ar_name = name.unwrap_or(path); 449 | if stat.is_file() { 450 | append_fs( 451 | dst, 452 | ar_name, 453 | &stat, 454 | &mut fs::File::open(path).await?, 455 | mode, 456 | None, 457 | ) 458 | .await?; 459 | Ok(()) 460 | } else if stat.is_dir() { 461 | append_fs(dst, ar_name, &stat, &mut io::empty(), mode, None).await?; 462 | Ok(()) 463 | } else if stat.file_type().is_symlink() { 464 | let link_name = fs::read_link(path).await?; 465 | append_fs( 466 | dst, 467 | ar_name, 468 | &stat, 469 | &mut io::empty(), 470 | mode, 471 | Some(&link_name), 472 | ) 473 | .await?; 474 | Ok(()) 475 | } else { 476 | Err(other(&format!("{} has unknown file type", path.display()))) 477 | } 478 | } 479 | 480 | async fn append_file( 481 | dst: &mut (dyn Write + Unpin + Send + Sync), 482 | path: &Path, 483 | file: &mut fs::File, 484 | mode: HeaderMode, 485 | ) -> io::Result<()> { 486 | let stat = file.metadata().await?; 487 | append_fs(dst, path, &stat, file, mode, None).await?; 488 | Ok(()) 489 | } 490 | 491 | async fn append_dir( 492 | dst: &mut (dyn Write + Unpin + Send + Sync), 493 | path: &Path, 494 | src_path: &Path, 495 | mode: HeaderMode, 496 | ) -> io::Result<()> { 497 | let stat = fs::metadata(src_path).await?; 498 | append_fs(dst, path, &stat, &mut io::empty(), mode, None).await?; 499 | Ok(()) 500 | } 501 | 502 | fn prepare_header(size: u64, entry_type: EntryType) -> Header { 503 | let mut header = Header::new_gnu(); 504 | let name = b"././@LongLink"; 505 | header.as_gnu_mut().unwrap().name[..name.len()].clone_from_slice(&name[..]); 506 | header.set_mode(0o644); 507 | header.set_uid(0); 508 | header.set_gid(0); 509 | header.set_mtime(0); 510 | // + 1 to be compliant with GNU tar 511 | header.set_size(size + 1); 512 | header.set_entry_type(entry_type); 513 | header.set_cksum(); 514 | header 515 | } 516 | 517 | async fn prepare_header_path( 518 | dst: &mut (dyn Write + Unpin + Send + Sync), 519 | header: &mut Header, 520 | path: &Path, 521 | ) -> io::Result<()> { 522 | // Try to encode the path directly in the header, but if it ends up not 523 | // working (probably because it's too long) then try to use the GNU-specific 524 | // long name extension by emitting an entry which indicates that it's the 525 | // filename. 526 | if let Err(e) = header.set_path(path) { 527 | let data = path2bytes(path)?; 528 | let max = header.as_old().name.len(); 529 | // Since e isn't specific enough to let us know the path is indeed too 530 | // long, verify it first before using the extension. 531 | if data.len() < max { 532 | return Err(e); 533 | } 534 | let header2 = prepare_header(data.len() as u64, EntryType::GNULongName); 535 | // null-terminated string 536 | let mut data2 = data.chain(io::repeat(0).take(1)); 537 | append(dst, &header2, &mut data2).await?; 538 | // Truncate the path to store in the header we're about to emit to 539 | // ensure we've got something at least mentioned. 540 | let path = bytes2path(Cow::Borrowed(&data[..max]))?; 541 | header.set_truncated_path_for_gnu_header(&path)?; 542 | } 543 | Ok(()) 544 | } 545 | 546 | async fn prepare_header_link( 547 | dst: &mut (dyn Write + Unpin + Send + Sync), 548 | header: &mut Header, 549 | link_name: &Path, 550 | ) -> io::Result<()> { 551 | // Same as previous function but for linkname 552 | if let Err(e) = header.set_link_name(link_name) { 553 | let data = path2bytes(link_name)?; 554 | if data.len() < header.as_old().linkname.len() { 555 | return Err(e); 556 | } 557 | let header2 = prepare_header(data.len() as u64, EntryType::GNULongLink); 558 | let mut data2 = data.chain(io::repeat(0).take(1)); 559 | append(dst, &header2, &mut data2).await?; 560 | } 561 | Ok(()) 562 | } 563 | 564 | async fn append_fs( 565 | dst: &mut (dyn Write + Unpin + Send + Sync), 566 | path: &Path, 567 | meta: &fs::Metadata, 568 | read: &mut (dyn Read + Unpin + Sync + Send), 569 | mode: HeaderMode, 570 | link_name: Option<&Path>, 571 | ) -> io::Result<()> { 572 | let mut header = Header::new_gnu(); 573 | 574 | prepare_header_path(dst, &mut header, path).await?; 575 | header.set_metadata_in_mode(meta, mode); 576 | if let Some(link_name) = link_name { 577 | prepare_header_link(dst, &mut header, link_name).await?; 578 | } 579 | header.set_cksum(); 580 | append(dst, &header, read).await?; 581 | 582 | Ok(()) 583 | } 584 | 585 | async fn append_dir_all( 586 | dst: &mut (dyn Write + Unpin + Send + Sync), 587 | path: &Path, 588 | src_path: &Path, 589 | mode: HeaderMode, 590 | follow: bool, 591 | ) -> io::Result<()> { 592 | let mut stack = vec![(src_path.to_path_buf(), true, false)]; 593 | while let Some((src, is_dir, is_symlink)) = stack.pop() { 594 | let dest = path.join(src.strip_prefix(src_path).unwrap()); 595 | 596 | // In case of a symlink pointing to a directory, is_dir is false, but src.is_dir() will return true 597 | if is_dir || (is_symlink && follow && src.is_dir().await) { 598 | let mut entries = fs::read_dir(&src).await?; 599 | while let Some(entry) = entries.next().await { 600 | let entry = entry?; 601 | let file_type = entry.file_type().await?; 602 | stack.push((entry.path(), file_type.is_dir(), file_type.is_symlink())); 603 | } 604 | if dest != Path::new("") { 605 | append_dir(dst, &dest, &src, mode).await?; 606 | } 607 | } else if !follow && is_symlink { 608 | let stat = fs::symlink_metadata(&src).await?; 609 | let link_name = fs::read_link(&src).await?; 610 | append_fs(dst, &dest, &stat, &mut io::empty(), mode, Some(&link_name)).await?; 611 | } else { 612 | append_file(dst, &dest, &mut fs::File::open(src).await?, mode).await?; 613 | } 614 | } 615 | Ok(()) 616 | } 617 | 618 | impl Drop for Builder { 619 | fn drop(&mut self) { 620 | async_std::task::block_on(async move { 621 | let _ = self.finish().await; 622 | }); 623 | } 624 | } 625 | 626 | #[cfg(test)] 627 | mod tests { 628 | use super::*; 629 | 630 | assert_impl_all!(async_std::fs::File: Send, Sync); 631 | assert_impl_all!(Builder: Send, Sync); 632 | } 633 | -------------------------------------------------------------------------------- /src/entry_type.rs: -------------------------------------------------------------------------------- 1 | // See https://en.wikipedia.org/wiki/Tar_%28computing%29#UStar_format 2 | /// Indicate for the type of file described by a header. 3 | /// 4 | /// Each `Header` has an `entry_type` method returning an instance of this type 5 | /// which can be used to inspect what the header is describing. 6 | 7 | /// A non-exhaustive enum representing the possible entry types 8 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 9 | #[non_exhaustive] 10 | pub enum EntryType { 11 | /// Regular file 12 | Regular, 13 | /// Hard link 14 | Link, 15 | /// Symbolic link 16 | Symlink, 17 | /// Character device 18 | Char, 19 | /// Block device 20 | Block, 21 | /// Directory 22 | Directory, 23 | /// Named pipe (fifo) 24 | Fifo, 25 | /// Implementation-defined 'high-performance' type, treated as regular file 26 | Continuous, 27 | /// GNU extension - long file name 28 | GNULongName, 29 | /// GNU extension - long link name (link target) 30 | GNULongLink, 31 | /// GNU extension - sparse file 32 | GNUSparse, 33 | /// Global extended header 34 | XGlobalHeader, 35 | /// Extended Header 36 | XHeader, 37 | /// Unknown header, 38 | Other(u8), 39 | } 40 | 41 | impl EntryType { 42 | /// Creates a new entry type from a raw byte. 43 | /// 44 | /// Note that the other named constructors of entry type may be more 45 | /// appropriate to create a file type from. 46 | pub fn new(byte: u8) -> EntryType { 47 | match byte { 48 | b'\x00' | b'0' => EntryType::Regular, 49 | b'1' => EntryType::Link, 50 | b'2' => EntryType::Symlink, 51 | b'3' => EntryType::Char, 52 | b'4' => EntryType::Block, 53 | b'5' => EntryType::Directory, 54 | b'6' => EntryType::Fifo, 55 | b'7' => EntryType::Continuous, 56 | b'x' => EntryType::XHeader, 57 | b'g' => EntryType::XGlobalHeader, 58 | b'L' => EntryType::GNULongName, 59 | b'K' => EntryType::GNULongLink, 60 | b'S' => EntryType::GNUSparse, 61 | other => EntryType::Other(other), 62 | } 63 | } 64 | 65 | /// Returns the raw underlying byte that this entry type represents. 66 | pub fn as_byte(self) -> u8 { 67 | match self { 68 | EntryType::Regular => b'0', 69 | EntryType::Link => b'1', 70 | EntryType::Symlink => b'2', 71 | EntryType::Char => b'3', 72 | EntryType::Block => b'4', 73 | EntryType::Directory => b'5', 74 | EntryType::Fifo => b'6', 75 | EntryType::Continuous => b'7', 76 | EntryType::XHeader => b'x', 77 | EntryType::XGlobalHeader => b'g', 78 | EntryType::GNULongName => b'L', 79 | EntryType::GNULongLink => b'K', 80 | EntryType::GNUSparse => b'S', 81 | EntryType::Other(other) => other, 82 | } 83 | } 84 | 85 | /// Creates a new entry type representing a regular file. 86 | pub fn file() -> EntryType { 87 | EntryType::Regular 88 | } 89 | 90 | /// Creates a new entry type representing a hard link. 91 | pub fn hard_link() -> EntryType { 92 | EntryType::Link 93 | } 94 | 95 | /// Creates a new entry type representing a symlink. 96 | pub fn symlink() -> EntryType { 97 | EntryType::Symlink 98 | } 99 | 100 | /// Creates a new entry type representing a character special device. 101 | pub fn character_special() -> EntryType { 102 | EntryType::Char 103 | } 104 | 105 | /// Creates a new entry type representing a block special device. 106 | pub fn block_special() -> EntryType { 107 | EntryType::Block 108 | } 109 | 110 | /// Creates a new entry type representing a directory. 111 | pub fn dir() -> EntryType { 112 | EntryType::Directory 113 | } 114 | 115 | /// Creates a new entry type representing a FIFO. 116 | pub fn fifo() -> EntryType { 117 | EntryType::Fifo 118 | } 119 | 120 | /// Creates a new entry type representing a contiguous file. 121 | pub fn contiguous() -> EntryType { 122 | EntryType::Continuous 123 | } 124 | 125 | /// Returns whether this type represents a regular file. 126 | pub fn is_file(self) -> bool { 127 | self == EntryType::Regular 128 | } 129 | 130 | /// Returns whether this type represents a hard link. 131 | pub fn is_hard_link(self) -> bool { 132 | self == EntryType::Link 133 | } 134 | 135 | /// Returns whether this type represents a symlink. 136 | pub fn is_symlink(self) -> bool { 137 | self == EntryType::Symlink 138 | } 139 | 140 | /// Returns whether this type represents a character special device. 141 | pub fn is_character_special(self) -> bool { 142 | self == EntryType::Char 143 | } 144 | 145 | /// Returns whether this type represents a block special device. 146 | pub fn is_block_special(self) -> bool { 147 | self == EntryType::Block 148 | } 149 | 150 | /// Returns whether this type represents a directory. 151 | pub fn is_dir(self) -> bool { 152 | self == EntryType::Directory 153 | } 154 | 155 | /// Returns whether this type represents a FIFO. 156 | pub fn is_fifo(self) -> bool { 157 | self == EntryType::Fifo 158 | } 159 | 160 | /// Returns whether this type represents a contiguous file. 161 | pub fn is_contiguous(self) -> bool { 162 | self == EntryType::Continuous 163 | } 164 | 165 | /// Returns whether this type represents a GNU long name header. 166 | pub fn is_gnu_longname(self) -> bool { 167 | self == EntryType::GNULongName 168 | } 169 | 170 | /// Returns whether this type represents a GNU sparse header. 171 | pub fn is_gnu_sparse(self) -> bool { 172 | self == EntryType::GNUSparse 173 | } 174 | 175 | /// Returns whether this type represents a GNU long link header. 176 | pub fn is_gnu_longlink(self) -> bool { 177 | self == EntryType::GNULongLink 178 | } 179 | 180 | /// Returns whether this type represents a GNU long name header. 181 | pub fn is_pax_global_extensions(self) -> bool { 182 | self == EntryType::XGlobalHeader 183 | } 184 | 185 | /// Returns whether this type represents a GNU long link header. 186 | pub fn is_pax_local_extensions(self) -> bool { 187 | self == EntryType::XHeader 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | 3 | use async_std::io::{self, Error}; 4 | 5 | #[derive(Debug)] 6 | pub struct TarError { 7 | desc: String, 8 | io: io::Error, 9 | } 10 | 11 | impl TarError { 12 | pub fn new(desc: &str, err: Error) -> TarError { 13 | TarError { 14 | desc: desc.to_string(), 15 | io: err, 16 | } 17 | } 18 | } 19 | 20 | impl error::Error for TarError { 21 | fn description(&self) -> &str { 22 | &self.desc 23 | } 24 | 25 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 26 | Some(&self.io) 27 | } 28 | } 29 | 30 | impl fmt::Display for TarError { 31 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 32 | self.desc.fmt(f) 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(t: TarError) -> Error { 38 | Error::new(t.io.kind(), t) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for reading and writing TAR archives in an async fashion. 2 | //! 3 | //! This library provides utilities necessary to manage [TAR archives][1] 4 | //! abstracted over a reader or writer. Great strides are taken to ensure that 5 | //! an archive is never required to be fully resident in memory, and all objects 6 | //! provide largely a streaming interface to read bytes from. 7 | //! 8 | //! [1]: http://en.wikipedia.org/wiki/Tar_%28computing%29 9 | 10 | // More docs about the detailed tar format can also be found here: 11 | // http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current 12 | 13 | // NB: some of the coding patterns and idioms here may seem a little strange. 14 | // This is currently attempting to expose a super generic interface while 15 | // also not forcing clients to codegen the entire crate each time they use 16 | // it. To that end lots of work is done to ensure that concrete 17 | // implementations are all found in this crate and the generic functions are 18 | // all just super thin wrappers (e.g. easy to codegen). 19 | 20 | #![deny(missing_docs)] 21 | #![deny(clippy::all)] 22 | 23 | use std::io::{Error, ErrorKind}; 24 | 25 | pub use crate::{ 26 | archive::{Archive, ArchiveBuilder, Entries}, 27 | builder::Builder, 28 | entry::{Entry, Unpacked}, 29 | entry_type::EntryType, 30 | header::{ 31 | GnuExtSparseHeader, GnuHeader, GnuSparseHeader, Header, HeaderMode, OldHeader, UstarHeader, 32 | }, 33 | pax::{PaxExtension, PaxExtensions}, 34 | }; 35 | 36 | mod archive; 37 | mod builder; 38 | mod entry; 39 | mod entry_type; 40 | mod error; 41 | mod header; 42 | mod pax; 43 | 44 | #[cfg(test)] 45 | #[macro_use] 46 | extern crate static_assertions; 47 | 48 | fn other(msg: &str) -> Error { 49 | Error::new(ErrorKind::Other, msg) 50 | } 51 | -------------------------------------------------------------------------------- /src/pax.rs: -------------------------------------------------------------------------------- 1 | use std::{slice, str}; 2 | 3 | use async_std::io; 4 | 5 | use crate::other; 6 | 7 | /// An iterator over the pax extensions in an archive entry. 8 | /// 9 | /// This iterator yields structures which can themselves be parsed into 10 | /// key/value pairs. 11 | pub struct PaxExtensions<'entry> { 12 | data: slice::Split<'entry, u8, fn(&u8) -> bool>, 13 | } 14 | 15 | /// A key/value pair corresponding to a pax extension. 16 | pub struct PaxExtension<'entry> { 17 | key: &'entry [u8], 18 | value: &'entry [u8], 19 | } 20 | 21 | pub fn pax_extensions(a: &[u8]) -> PaxExtensions { 22 | PaxExtensions { 23 | data: a.split(|a| *a == b'\n'), 24 | } 25 | } 26 | 27 | impl<'entry> Iterator for PaxExtensions<'entry> { 28 | type Item = io::Result>; 29 | 30 | fn next(&mut self) -> Option>> { 31 | let line = match self.data.next() { 32 | Some(line) => { 33 | if line.is_empty() { 34 | return None; 35 | } else { 36 | line 37 | } 38 | } 39 | None => return None, 40 | }; 41 | 42 | Some( 43 | line.iter() 44 | .position(|b| *b == b' ') 45 | .and_then(|i| { 46 | str::from_utf8(&line[..i]) 47 | .ok() 48 | .and_then(|len| len.parse::().ok().map(|j| (i + 1, j))) 49 | }) 50 | .and_then(|(kvstart, reported_len)| { 51 | if line.len() + 1 == reported_len { 52 | line[kvstart..] 53 | .iter() 54 | .position(|b| *b == b'=') 55 | .map(|equals| (kvstart, equals)) 56 | } else { 57 | None 58 | } 59 | }) 60 | .map(|(kvstart, equals)| PaxExtension { 61 | key: &line[kvstart..kvstart + equals], 62 | value: &line[kvstart + equals + 1..], 63 | }) 64 | .ok_or_else(|| other("malformed pax extension")), 65 | ) 66 | } 67 | } 68 | 69 | impl<'entry> PaxExtension<'entry> { 70 | /// Returns the key for this key/value pair parsed as a string. 71 | /// 72 | /// May fail if the key isn't actually utf-8. 73 | pub fn key(&self) -> Result<&'entry str, str::Utf8Error> { 74 | str::from_utf8(self.key) 75 | } 76 | 77 | /// Returns the underlying raw bytes for the key of this key/value pair. 78 | pub fn key_bytes(&self) -> &'entry [u8] { 79 | self.key 80 | } 81 | 82 | /// Returns the value for this key/value pair parsed as a string. 83 | /// 84 | /// May fail if the value isn't actually utf-8. 85 | pub fn value(&self) -> Result<&'entry str, str::Utf8Error> { 86 | str::from_utf8(self.value) 87 | } 88 | 89 | /// Returns the underlying raw bytes for this value of this key/value pair. 90 | pub fn value_bytes(&self) -> &'entry [u8] { 91 | self.value 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/archives/7z_long_path.tar: -------------------------------------------------------------------------------- 1 | ././@LongLink0040777000000000000000000000015013661252300010077 Lustar00this_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_name/this_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_n0040777000000000000000000000000013661252300032242 5ustar00././@LongLink0100777000000000000000000000016013661252300010075 Lustar00this_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_name/test.txtthis_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_n0100777000000000000000000000000013661252300032232 0ustar00 -------------------------------------------------------------------------------- /tests/archives/directory.tar: -------------------------------------------------------------------------------- 1 | a/0000755000175000017500000000000012440021574010647 5ustar mvdnesmvdnesa/b/0000755000175000017500000000000012440021525011064 5ustar mvdnesmvdnesa/c0000644000175000017500000000000212440021631010776 0ustar mvdnesmvdnesc 2 | -------------------------------------------------------------------------------- /tests/archives/duplicate_dirs.tar: -------------------------------------------------------------------------------- 1 | some_dir/000755 000766 000024 00000000000 12527333503 015306 5ustar00cadencemarseillestaff000000 000000 some_dir/000755 000766 000024 00000000000 12527333503 015306 5ustar00cadencemarseillestaff000000 000000 -------------------------------------------------------------------------------- /tests/archives/empty_filename.tar: -------------------------------------------------------------------------------- 1 | ustar 0000755000175000017500000000000012440021574010647 5ustar mvd42530647 5 -------------------------------------------------------------------------------- /tests/archives/file_times.tar: -------------------------------------------------------------------------------- 1 | a000644 000765 000024 00000000000 07346545000 012100 0ustar00HyeonKimstaff000000 000000 -------------------------------------------------------------------------------- /tests/archives/link.tar: -------------------------------------------------------------------------------- 1 | lnk0000777000175000017500000000000012624625331010546 2fileustar devdevfile0000644000175000017500000000001612624625317010043 0ustar devdevfile_contents 2 | -------------------------------------------------------------------------------- /tests/archives/pax.tar: -------------------------------------------------------------------------------- 1 | ./PaxHeaders.4428/Cargo.toml0000644000000000000000000000013112647240064012625 xustar0030 mtime=1453146164.953123768 2 | 29 atime=1453251915.24892486 3 | 30 ctime=1453146164.953123768 4 | Cargo.toml0000664000175000017500000000137012647240064012762 0ustar00alexalex00000000000000[package] 5 | 6 | name = "tar" 7 | version = "0.3.3" 8 | authors = ["Alex Crichton "] 9 | homepage = "https://github.com/alexcrichton/tar-rs" 10 | repository = "https://github.com/alexcrichton/tar-rs" 11 | documentation = "http://alexcrichton.com/tar-rs" 12 | license = "MIT/Apache-2.0" 13 | keywords = ["tar", "tarfile", "encoding"] 14 | readme = "README.md" 15 | 16 | description = """ 17 | A Rust implementation of a TAR file reader and writer. This library does not 18 | currently handle compression, but it is abstract over all I/O readers and 19 | writers. Additionally, great lengths are taken to ensure that the entire 20 | contents are never required to be entirely resident in memory all at once. 21 | """ 22 | 23 | [dependencies] 24 | libc = "0.2" 25 | filetime = "0.1.5" 26 | winapi = "0.2" 27 | 28 | [dev-dependencies] 29 | tempdir = "0.3" 30 | src/PaxHeaders.4428/lib.rs0000644000000000000000000000013212647616024012447 xustar0030 mtime=1453267988.890050911 31 | 30 atime=1453268000.730238913 32 | 30 ctime=1453267988.898051038 33 | src/lib.rs0000664000175000017500000000452612647616024012746 0ustar00alexalex00000000000000//! A library for reading and writing TAR archives 34 | //! 35 | //! This library provides utilities necessary to manage TAR archives [1] 36 | //! abstracted over a reader or writer. Great strides are taken to ensure that 37 | //! an archive is never required to be fully resident in memory, all objects 38 | //! provide largely a streaming interface to read bytes from. 39 | //! 40 | //! [1]: http://en.wikipedia.org/wiki/Tar_%28computing%29 41 | 42 | // More docs about the detailed tar format can also be found here: 43 | // http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current 44 | 45 | // NB: some of the coding patterns and idioms here may seem a little strange. 46 | // This is currently attempting to expose a super generic interface while 47 | // also not forcing clients to codegen the entire crate each time they use 48 | // it. To that end lots of work is done to ensure that concrete 49 | // implementations are all found in this crate and the generic functions are 50 | // all just super thin wrappers (e.g. easy to codegen). 51 | 52 | #![doc(html_root_url = "http://alexcrichton.com/tar-rs")] 53 | #![deny(missing_docs)] 54 | #![cfg_attr(test, deny(warnings))] 55 | 56 | extern crate libc; 57 | extern crate winapi; 58 | extern crate filetime; 59 | 60 | use std::io::{Error, ErrorKind}; 61 | use std::ops::{Deref, DerefMut}; 62 | 63 | pub use header::{Header, UstarHeader, GnuHeader, GnuSparseHeader}; 64 | pub use entry_type::EntryType; 65 | pub use entry::Entry; 66 | pub use archive::{Archive, Entries}; 67 | pub use builder::Builder; 68 | pub use pax::{PaxExtensions, PaxExtension}; 69 | 70 | mod archive; 71 | mod builder; 72 | mod entry; 73 | mod entry_type; 74 | mod error; 75 | mod header; 76 | mod pax; 77 | 78 | // FIXME(rust-lang/rust#26403): 79 | // Right now there's a bug when a DST struct's last field has more 80 | // alignment than the rest of a structure, causing invalid pointers to be 81 | // created when it's casted around at runtime. To work around this we force 82 | // our DST struct to instead have a forcibly higher alignment via a 83 | // synthesized u64 (hopefully the largest alignment we'll run into in 84 | // practice), and this should hopefully ensure that the pointers all work 85 | // out. 86 | struct AlignHigher(u64, R); 87 | 88 | impl Deref for AlignHigher { 89 | type Target = R; 90 | fn deref(&self) -> &R { &self.1 } 91 | } 92 | impl DerefMut for AlignHigher { 93 | fn deref_mut(&mut self) -> &mut R { &mut self.1 } 94 | } 95 | 96 | fn other(msg: &str) -> Error { 97 | Error::new(ErrorKind::Other, msg) 98 | } 99 | -------------------------------------------------------------------------------- /tests/archives/pax2.tar: -------------------------------------------------------------------------------- 1 | aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000644000000000000000000000044413315003211030375 xustar00202 path=aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/ 2 | 30 mtime=1530136201.099232071 3 | 30 atime=1530136209.343356252 4 | 30 ctime=1530136201.099232071 5 | aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000775000175000017500000000000013315003211033301 5ustar00alexalex00000000000000 -------------------------------------------------------------------------------- /tests/archives/reading_files.tar: -------------------------------------------------------------------------------- 1 | a0000664000175000017500000000002612361754471007677 0ustar alexalexa 2 | a 3 | a 4 | a 5 | a 6 | a 7 | a 8 | a 9 | a 10 | a 11 | a 12 | b0000664000175000017500000000002612361754476007705 0ustar alexalexb 13 | b 14 | b 15 | b 16 | b 17 | b 18 | b 19 | b 20 | b 21 | b 22 | b 23 | -------------------------------------------------------------------------------- /tests/archives/simple.tar: -------------------------------------------------------------------------------- 1 | a0000664000175000017500000000000012361661662007666 0ustar alexalexb0000664000175000017500000000000012361661662007667 0ustar alexalexc0000664000175000017500000000000012361661662007670 0ustar alexalex -------------------------------------------------------------------------------- /tests/archives/simple_missing_last_header.tar: -------------------------------------------------------------------------------- 1 | a0000664000175000017500000000000012361661662007666 0ustar alexalexb0000664000175000017500000000000012361661662007667 0ustar alexalexc0000664000175000017500000000000012361661662007670 0ustar alexalex -------------------------------------------------------------------------------- /tests/archives/spaces.tar: -------------------------------------------------------------------------------- 1 | a100777 0 0 2 12440016664 4253 0a 2 | -------------------------------------------------------------------------------- /tests/archives/sparse.tar: -------------------------------------------------------------------------------- 1 | sparse_begin.txt0000644000175000001440000000100012710737150020023 Sustar pcusers0000000000000000001000000000176400000000000000000017640test 2 | sparse_end.txt0000644000175000001440000000065112710737305015465 Sustar pcusers000000170000000000065100000017651test_end 3 | sparse_ext.txt0000644000175000001440000000500512710742433023641 Sustar pcusers00000010000000000010000000003000000000001000000000500000000000100000000070000000000010000000013000500000110000000000010000000013000000000000005text 4 | text 5 | text 6 | text 7 | text 8 | text 9 | sparse.txt0000644000175000001440000000200012710737456020710 Sustar pcusers00000010000000000010000000002700000000001000000000400000000000000000000040000hello 10 | world 11 | -------------------------------------------------------------------------------- /tests/archives/xattrs.tar: -------------------------------------------------------------------------------- 1 | ./PaxHeaders.8071/a0000644000000000000000000000013112727267131011041 xustar0030 mtime=1465740889.394991526 2 | 29 atime=1465740884.12838135 3 | 30 ctime=1465740889.394991526 4 | a/0000755000175000001440000000000012727267131011477 5ustar00muteusers00000000000000a/PaxHeaders.8071/b0000644000000000000000000000017512727267131011135 xustar0030 mtime=1465740889.394991526 5 | 30 atime=1465740889.394991526 6 | 30 ctime=1465740898.211565394 7 | 35 SCHILY.xattr.user.pax.flags=epm 8 | a/b0000644000175000001440000000000012727267131011631 0ustar00muteusers00000000000000 -------------------------------------------------------------------------------- /tests/entry.rs: -------------------------------------------------------------------------------- 1 | extern crate async_tar; 2 | extern crate tempfile; 3 | 4 | use async_std::{ 5 | fs::{create_dir, File}, 6 | prelude::*, 7 | }; 8 | 9 | use tempfile::Builder; 10 | 11 | macro_rules! t { 12 | ($e:expr) => { 13 | match $e { 14 | Ok(v) => v, 15 | Err(e) => panic!("{} returned {}", stringify!($e), e), 16 | } 17 | }; 18 | } 19 | 20 | #[async_std::test] 21 | async fn absolute_symlink() { 22 | let mut ar = async_tar::Builder::new(Vec::new()); 23 | 24 | let mut header = async_tar::Header::new_gnu(); 25 | header.set_size(0); 26 | header.set_entry_type(async_tar::EntryType::Symlink); 27 | t!(header.set_path("foo")); 28 | t!(header.set_link_name("/bar")); 29 | header.set_cksum(); 30 | t!(ar.append(&header, &[][..]).await); 31 | 32 | let bytes = t!(ar.into_inner().await); 33 | let ar = async_tar::Archive::new(&bytes[..]); 34 | 35 | let td = t!(Builder::new().prefix("tar").tempdir()); 36 | t!(ar.unpack(td.path()).await); 37 | 38 | t!(td.path().join("foo").symlink_metadata()); 39 | 40 | let ar = async_tar::Archive::new(&bytes[..]); 41 | let mut entries = t!(ar.entries()); 42 | let entry = t!(entries.next().await.unwrap()); 43 | assert_eq!(&*entry.link_name_bytes().unwrap(), b"/bar"); 44 | } 45 | 46 | #[async_std::test] 47 | async fn absolute_hardlink() { 48 | let td = t!(Builder::new().prefix("tar").tempdir()); 49 | let mut ar = async_tar::Builder::new(Vec::new()); 50 | 51 | let mut header = async_tar::Header::new_gnu(); 52 | header.set_size(0); 53 | header.set_entry_type(async_tar::EntryType::Regular); 54 | t!(header.set_path("foo")); 55 | header.set_cksum(); 56 | t!(ar.append(&header, &[][..]).await); 57 | 58 | let mut header = async_tar::Header::new_gnu(); 59 | header.set_size(0); 60 | header.set_entry_type(async_tar::EntryType::Link); 61 | t!(header.set_path("bar")); 62 | // This absolute path under tempdir will be created at unpack time 63 | t!(header.set_link_name(td.path().join("foo"))); 64 | header.set_cksum(); 65 | t!(ar.append(&header, &[][..]).await); 66 | 67 | let bytes = t!(ar.into_inner().await); 68 | let ar = async_tar::Archive::new(&bytes[..]); 69 | 70 | t!(ar.unpack(td.path()).await); 71 | t!(td.path().join("foo").metadata()); 72 | t!(td.path().join("bar").metadata()); 73 | } 74 | 75 | #[async_std::test] 76 | async fn relative_hardlink() { 77 | let mut ar = async_tar::Builder::new(Vec::new()); 78 | 79 | let mut header = async_tar::Header::new_gnu(); 80 | header.set_size(0); 81 | header.set_entry_type(async_tar::EntryType::Regular); 82 | t!(header.set_path("foo")); 83 | header.set_cksum(); 84 | t!(ar.append(&header, &[][..]).await); 85 | 86 | let mut header = async_tar::Header::new_gnu(); 87 | header.set_size(0); 88 | header.set_entry_type(async_tar::EntryType::Link); 89 | t!(header.set_path("bar")); 90 | t!(header.set_link_name("foo")); 91 | header.set_cksum(); 92 | t!(ar.append(&header, &[][..]).await); 93 | 94 | let bytes = t!(ar.into_inner().await); 95 | let ar = async_tar::Archive::new(&bytes[..]); 96 | 97 | let td = t!(Builder::new().prefix("tar").tempdir()); 98 | t!(ar.unpack(td.path()).await); 99 | t!(td.path().join("foo").metadata()); 100 | t!(td.path().join("bar").metadata()); 101 | } 102 | 103 | #[async_std::test] 104 | async fn absolute_link_deref_error() { 105 | let mut ar = async_tar::Builder::new(Vec::new()); 106 | 107 | let mut header = async_tar::Header::new_gnu(); 108 | header.set_size(0); 109 | header.set_entry_type(async_tar::EntryType::Symlink); 110 | t!(header.set_path("foo")); 111 | t!(header.set_link_name("/")); 112 | header.set_cksum(); 113 | t!(ar.append(&header, &[][..]).await); 114 | 115 | let mut header = async_tar::Header::new_gnu(); 116 | header.set_size(0); 117 | header.set_entry_type(async_tar::EntryType::Regular); 118 | t!(header.set_path("foo/bar")); 119 | header.set_cksum(); 120 | t!(ar.append(&header, &[][..]).await); 121 | 122 | let bytes = t!(ar.into_inner().await); 123 | let ar = async_tar::Archive::new(&bytes[..]); 124 | 125 | let td = t!(Builder::new().prefix("tar").tempdir()); 126 | assert!(ar.unpack(td.path()).await.is_err()); 127 | t!(td.path().join("foo").symlink_metadata()); 128 | assert!(File::open(td.path().join("foo").join("bar")).await.is_err()); 129 | } 130 | 131 | #[async_std::test] 132 | async fn relative_link_deref_error() { 133 | let mut ar = async_tar::Builder::new(Vec::new()); 134 | 135 | let mut header = async_tar::Header::new_gnu(); 136 | header.set_size(0); 137 | header.set_entry_type(async_tar::EntryType::Symlink); 138 | t!(header.set_path("foo")); 139 | t!(header.set_link_name("../../../../")); 140 | header.set_cksum(); 141 | t!(ar.append(&header, &[][..]).await); 142 | 143 | let mut header = async_tar::Header::new_gnu(); 144 | header.set_size(0); 145 | header.set_entry_type(async_tar::EntryType::Regular); 146 | t!(header.set_path("foo/bar")); 147 | header.set_cksum(); 148 | t!(ar.append(&header, &[][..]).await); 149 | 150 | let bytes = t!(ar.into_inner().await); 151 | let ar = async_tar::Archive::new(&bytes[..]); 152 | 153 | let td = t!(Builder::new().prefix("tar").tempdir()); 154 | assert!(ar.unpack(td.path()).await.is_err()); 155 | t!(td.path().join("foo").symlink_metadata()); 156 | assert!(File::open(td.path().join("foo").join("bar")).await.is_err()); 157 | } 158 | 159 | #[async_std::test] 160 | #[cfg(unix)] 161 | async fn directory_maintains_permissions() { 162 | use ::std::os::unix::fs::PermissionsExt; 163 | 164 | let mut ar = async_tar::Builder::new(Vec::new()); 165 | 166 | let mut header = async_tar::Header::new_gnu(); 167 | header.set_size(0); 168 | header.set_entry_type(async_tar::EntryType::Directory); 169 | t!(header.set_path("foo")); 170 | header.set_mode(0o777); 171 | header.set_cksum(); 172 | t!(ar.append(&header, &[][..]).await); 173 | 174 | let bytes = t!(ar.into_inner().await); 175 | let ar = async_tar::Archive::new(&bytes[..]); 176 | 177 | let td = t!(Builder::new().prefix("tar").tempdir()); 178 | t!(ar.unpack(td.path()).await); 179 | let f = t!(File::open(td.path().join("foo")).await); 180 | let md = t!(f.metadata().await); 181 | assert!(md.is_dir()); 182 | assert_eq!(md.permissions().mode(), 0o40777); 183 | } 184 | 185 | #[async_std::test] 186 | #[cfg(not(windows))] // dangling symlinks have weird permissions 187 | async fn modify_link_just_created() { 188 | let mut ar = async_tar::Builder::new(Vec::new()); 189 | 190 | let mut header = async_tar::Header::new_gnu(); 191 | header.set_size(0); 192 | header.set_entry_type(async_tar::EntryType::Symlink); 193 | t!(header.set_path("foo")); 194 | t!(header.set_link_name("bar")); 195 | header.set_cksum(); 196 | t!(ar.append(&header, &[][..]).await); 197 | 198 | let mut header = async_tar::Header::new_gnu(); 199 | header.set_size(0); 200 | header.set_entry_type(async_tar::EntryType::Regular); 201 | t!(header.set_path("bar/foo")); 202 | header.set_cksum(); 203 | t!(ar.append(&header, &[][..]).await); 204 | 205 | let mut header = async_tar::Header::new_gnu(); 206 | header.set_size(0); 207 | header.set_entry_type(async_tar::EntryType::Regular); 208 | t!(header.set_path("foo/bar")); 209 | header.set_cksum(); 210 | t!(ar.append(&header, &[][..]).await); 211 | 212 | let bytes = t!(ar.into_inner().await); 213 | let ar = async_tar::Archive::new(&bytes[..]); 214 | 215 | let td = t!(Builder::new().prefix("tar").tempdir()); 216 | t!(ar.unpack(td.path()).await); 217 | 218 | t!(File::open(td.path().join("bar/foo")).await); 219 | t!(File::open(td.path().join("bar/bar")).await); 220 | t!(File::open(td.path().join("foo/foo")).await); 221 | t!(File::open(td.path().join("foo/bar")).await); 222 | } 223 | 224 | #[async_std::test] 225 | #[cfg(not(windows))] // dangling symlinks have weird permissions 226 | async fn modify_outside_with_relative_symlink() { 227 | let mut ar = async_tar::Builder::new(Vec::new()); 228 | 229 | let mut header = async_tar::Header::new_gnu(); 230 | header.set_size(0); 231 | header.set_entry_type(async_tar::EntryType::Symlink); 232 | t!(header.set_path("symlink")); 233 | t!(header.set_link_name("..")); 234 | header.set_cksum(); 235 | t!(ar.append(&header, &[][..]).await); 236 | 237 | let mut header = async_tar::Header::new_gnu(); 238 | header.set_size(0); 239 | header.set_entry_type(async_tar::EntryType::Regular); 240 | t!(header.set_path("symlink/foo/bar")); 241 | header.set_cksum(); 242 | t!(ar.append(&header, &[][..]).await); 243 | 244 | let bytes = t!(ar.into_inner().await); 245 | let ar = async_tar::Archive::new(&bytes[..]); 246 | 247 | let td = t!(Builder::new().prefix("tar").tempdir()); 248 | let tar_dir = td.path().join("tar"); 249 | create_dir(&tar_dir).await.unwrap(); 250 | assert!(ar.unpack(tar_dir).await.is_err()); 251 | assert!(!td.path().join("foo").exists()); 252 | } 253 | 254 | #[async_std::test] 255 | async fn parent_paths_error() { 256 | let mut ar = async_tar::Builder::new(Vec::new()); 257 | 258 | let mut header = async_tar::Header::new_gnu(); 259 | header.set_size(0); 260 | header.set_entry_type(async_tar::EntryType::Symlink); 261 | t!(header.set_path("foo")); 262 | t!(header.set_link_name("..")); 263 | header.set_cksum(); 264 | t!(ar.append(&header, &[][..]).await); 265 | 266 | let mut header = async_tar::Header::new_gnu(); 267 | header.set_size(0); 268 | header.set_entry_type(async_tar::EntryType::Regular); 269 | t!(header.set_path("foo/bar")); 270 | header.set_cksum(); 271 | t!(ar.append(&header, &[][..]).await); 272 | 273 | let bytes = t!(ar.into_inner().await); 274 | let ar = async_tar::Archive::new(&bytes[..]); 275 | 276 | let td = t!(Builder::new().prefix("tar").tempdir()); 277 | assert!(ar.unpack(td.path()).await.is_err()); 278 | t!(td.path().join("foo").symlink_metadata()); 279 | assert!(File::open(td.path().join("foo").join("bar")).await.is_err()); 280 | } 281 | 282 | #[async_std::test] 283 | #[cfg(unix)] 284 | async fn good_parent_paths_ok() { 285 | use std::path::PathBuf; 286 | let mut ar = async_tar::Builder::new(Vec::new()); 287 | 288 | let mut header = async_tar::Header::new_gnu(); 289 | header.set_size(0); 290 | header.set_entry_type(async_tar::EntryType::Symlink); 291 | t!(header.set_path(PathBuf::from("foo").join("bar"))); 292 | t!(header.set_link_name(PathBuf::from("..").join("bar"))); 293 | header.set_cksum(); 294 | t!(ar.append(&header, &[][..]).await); 295 | 296 | let mut header = async_tar::Header::new_gnu(); 297 | header.set_size(0); 298 | header.set_entry_type(async_tar::EntryType::Regular); 299 | t!(header.set_path("bar")); 300 | header.set_cksum(); 301 | t!(ar.append(&header, &[][..]).await); 302 | 303 | let bytes = t!(ar.into_inner().await); 304 | let ar = async_tar::Archive::new(&bytes[..]); 305 | 306 | let td = t!(Builder::new().prefix("tar").tempdir()); 307 | t!(ar.unpack(td.path()).await); 308 | t!(td.path().join("foo").join("bar").read_link()); 309 | let dst = t!(td.path().join("foo").join("bar").canonicalize()); 310 | t!(File::open(dst).await); 311 | } 312 | 313 | #[async_std::test] 314 | async fn modify_hard_link_just_created() { 315 | let mut ar = async_tar::Builder::new(Vec::new()); 316 | 317 | let mut header = async_tar::Header::new_gnu(); 318 | header.set_size(0); 319 | header.set_entry_type(async_tar::EntryType::Link); 320 | t!(header.set_path("foo")); 321 | t!(header.set_link_name("../test")); 322 | header.set_cksum(); 323 | t!(ar.append(&header, &[][..]).await); 324 | 325 | let mut header = async_tar::Header::new_gnu(); 326 | header.set_size(1); 327 | header.set_entry_type(async_tar::EntryType::Regular); 328 | t!(header.set_path("foo")); 329 | header.set_cksum(); 330 | t!(ar.append(&header, &b"x"[..]).await); 331 | 332 | let bytes = t!(ar.into_inner().await); 333 | let ar = async_tar::Archive::new(&bytes[..]); 334 | 335 | let td = t!(Builder::new().prefix("tar").tempdir()); 336 | 337 | let test = td.path().join("test"); 338 | t!(File::create(&test).await); 339 | 340 | let dir = td.path().join("dir"); 341 | assert!(ar.unpack(&dir).await.is_err()); 342 | 343 | let mut contents = Vec::new(); 344 | t!(t!(File::open(&test).await).read_to_end(&mut contents).await); 345 | assert_eq!(contents.len(), 0); 346 | } 347 | 348 | #[async_std::test] 349 | async fn modify_symlink_just_created() { 350 | let mut ar = async_tar::Builder::new(Vec::new()); 351 | 352 | let mut header = async_tar::Header::new_gnu(); 353 | header.set_size(0); 354 | header.set_entry_type(async_tar::EntryType::Symlink); 355 | t!(header.set_path("foo")); 356 | t!(header.set_link_name("../test")); 357 | header.set_cksum(); 358 | t!(ar.append(&header, &[][..]).await); 359 | 360 | let mut header = async_tar::Header::new_gnu(); 361 | header.set_size(1); 362 | header.set_entry_type(async_tar::EntryType::Regular); 363 | t!(header.set_path("foo")); 364 | header.set_cksum(); 365 | t!(ar.append(&header, &b"x"[..]).await); 366 | 367 | let bytes = t!(ar.into_inner().await); 368 | let ar = async_tar::Archive::new(&bytes[..]); 369 | 370 | let td = t!(Builder::new().prefix("tar").tempdir()); 371 | 372 | let test = td.path().join("test"); 373 | t!(File::create(&test).await); 374 | 375 | let dir = td.path().join("dir"); 376 | t!(ar.unpack(&dir).await); 377 | 378 | let mut contents = Vec::new(); 379 | t!(t!(File::open(&test).await).read_to_end(&mut contents).await); 380 | assert_eq!(contents.len(), 0); 381 | } 382 | -------------------------------------------------------------------------------- /tests/header/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | 3 | use std::{ 4 | fs::{self, File}, 5 | io::{self, Write}, 6 | mem, 7 | path::Path, 8 | thread, time, 9 | }; 10 | 11 | use tempfile::Builder; 12 | 13 | use async_tar::{GnuHeader, Header, HeaderMode}; 14 | 15 | #[test] 16 | fn default_gnu() { 17 | let mut h = Header::new_gnu(); 18 | assert!(h.as_gnu().is_some()); 19 | assert!(h.as_gnu_mut().is_some()); 20 | assert!(h.as_ustar().is_none()); 21 | assert!(h.as_ustar_mut().is_none()); 22 | } 23 | 24 | #[test] 25 | fn goto_old() { 26 | let mut h = Header::new_old(); 27 | assert!(h.as_gnu().is_none()); 28 | assert!(h.as_gnu_mut().is_none()); 29 | assert!(h.as_ustar().is_none()); 30 | assert!(h.as_ustar_mut().is_none()); 31 | } 32 | 33 | #[test] 34 | fn goto_ustar() { 35 | let mut h = Header::new_ustar(); 36 | assert!(h.as_gnu().is_none()); 37 | assert!(h.as_gnu_mut().is_none()); 38 | assert!(h.as_ustar().is_some()); 39 | assert!(h.as_ustar_mut().is_some()); 40 | } 41 | 42 | #[test] 43 | fn link_name() { 44 | let mut h = Header::new_gnu(); 45 | t!(h.set_link_name("foo")); 46 | assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo")); 47 | t!(h.set_link_name("../foo")); 48 | assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("../foo")); 49 | t!(h.set_link_name("foo/bar")); 50 | assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo/bar")); 51 | t!(h.set_link_name("foo\\ba")); 52 | if cfg!(windows) { 53 | assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo/ba")); 54 | } else { 55 | assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo\\ba")); 56 | } 57 | 58 | let name = "foo\\bar\0"; 59 | for (slot, val) in h.as_old_mut().linkname.iter_mut().zip(name.as_bytes()) { 60 | *slot = *val; 61 | } 62 | assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo\\bar")); 63 | 64 | assert!(h.set_link_name("\0").is_err()); 65 | } 66 | 67 | #[test] 68 | fn mtime() { 69 | let h = Header::new_gnu(); 70 | assert_eq!(t!(h.mtime()), 0); 71 | 72 | let h = Header::new_ustar(); 73 | assert_eq!(t!(h.mtime()), 0); 74 | 75 | let h = Header::new_old(); 76 | assert_eq!(t!(h.mtime()), 0); 77 | } 78 | 79 | #[test] 80 | fn user_and_group_name() { 81 | let mut h = Header::new_gnu(); 82 | t!(h.set_username("foo")); 83 | t!(h.set_groupname("bar")); 84 | assert_eq!(t!(h.username()), Some("foo")); 85 | assert_eq!(t!(h.groupname()), Some("bar")); 86 | 87 | h = Header::new_ustar(); 88 | t!(h.set_username("foo")); 89 | t!(h.set_groupname("bar")); 90 | assert_eq!(t!(h.username()), Some("foo")); 91 | assert_eq!(t!(h.groupname()), Some("bar")); 92 | 93 | h = Header::new_old(); 94 | assert_eq!(t!(h.username()), None); 95 | assert_eq!(t!(h.groupname()), None); 96 | assert!(h.set_username("foo").is_err()); 97 | assert!(h.set_groupname("foo").is_err()); 98 | } 99 | 100 | #[test] 101 | fn dev_major_minor() { 102 | let mut h = Header::new_gnu(); 103 | t!(h.set_device_major(1)); 104 | t!(h.set_device_minor(2)); 105 | assert_eq!(t!(h.device_major()), Some(1)); 106 | assert_eq!(t!(h.device_minor()), Some(2)); 107 | 108 | h = Header::new_ustar(); 109 | t!(h.set_device_major(1)); 110 | t!(h.set_device_minor(2)); 111 | assert_eq!(t!(h.device_major()), Some(1)); 112 | assert_eq!(t!(h.device_minor()), Some(2)); 113 | 114 | h.as_ustar_mut().unwrap().dev_minor[0] = 0x7f; 115 | h.as_ustar_mut().unwrap().dev_major[0] = 0x7f; 116 | assert!(h.device_major().is_err()); 117 | assert!(h.device_minor().is_err()); 118 | 119 | h.as_ustar_mut().unwrap().dev_minor[0] = b'g'; 120 | h.as_ustar_mut().unwrap().dev_major[0] = b'h'; 121 | assert!(h.device_major().is_err()); 122 | assert!(h.device_minor().is_err()); 123 | 124 | h = Header::new_old(); 125 | assert_eq!(t!(h.device_major()), None); 126 | assert_eq!(t!(h.device_minor()), None); 127 | assert!(h.set_device_major(1).is_err()); 128 | assert!(h.set_device_minor(1).is_err()); 129 | } 130 | 131 | #[test] 132 | fn set_path() { 133 | let mut h = Header::new_gnu(); 134 | t!(h.set_path("foo")); 135 | assert_eq!(t!(h.path()).to_str(), Some("foo")); 136 | t!(h.set_path("foo/")); 137 | assert_eq!(t!(h.path()).to_str(), Some("foo/")); 138 | t!(h.set_path("foo/bar")); 139 | assert_eq!(t!(h.path()).to_str(), Some("foo/bar")); 140 | t!(h.set_path("foo\\bar")); 141 | if cfg!(windows) { 142 | assert_eq!(t!(h.path()).to_str(), Some("foo/bar")); 143 | } else { 144 | assert_eq!(t!(h.path()).to_str(), Some("foo\\bar")); 145 | } 146 | 147 | let long_name = "foo".repeat(100); 148 | let medium1 = "foo".repeat(52); 149 | let medium2 = "fo/".repeat(52); 150 | 151 | assert!(h.set_path(&long_name).is_err()); 152 | assert!(h.set_path(&medium1).is_err()); 153 | assert!(h.set_path(&medium2).is_err()); 154 | assert!(h.set_path("\0").is_err()); 155 | 156 | assert!(h.set_path("..").is_err()); 157 | assert!(h.set_path("foo/..").is_err()); 158 | assert!(h.set_path("foo/../bar").is_err()); 159 | 160 | h = Header::new_ustar(); 161 | t!(h.set_path("foo")); 162 | assert_eq!(t!(h.path()).to_str(), Some("foo")); 163 | 164 | assert!(h.set_path(&long_name).is_err()); 165 | assert!(h.set_path(&medium1).is_err()); 166 | t!(h.set_path(&medium2)); 167 | assert_eq!(t!(h.path()).to_str(), Some(&medium2[..])); 168 | } 169 | 170 | #[test] 171 | fn set_ustar_path_hard() { 172 | let mut h = Header::new_ustar(); 173 | let p = Path::new("a").join(vec!["a"; 100].join("")); 174 | t!(h.set_path(&p)); 175 | let path = t!(h.path()); 176 | let actual: &Path = path.as_ref().into(); 177 | assert_eq!(actual, p); 178 | } 179 | 180 | #[test] 181 | fn set_metadata_deterministic() { 182 | let td = t!(Builder::new().prefix("async-tar").tempdir()); 183 | let tmppath = td.path().join("tmpfile"); 184 | 185 | fn mk_header(path: &Path, readonly: bool) -> Result { 186 | let mut file = t!(File::create(path)); 187 | t!(file.write_all(b"c")); 188 | let mut perms = t!(file.metadata()).permissions(); 189 | perms.set_readonly(readonly); 190 | t!(fs::set_permissions(path, perms)); 191 | let mut h = Header::new_ustar(); 192 | h.set_metadata_in_mode(&t!(path.metadata()), HeaderMode::Deterministic); 193 | Ok(h) 194 | } 195 | 196 | // Create "the same" File twice in a row, one second apart, with differing readonly values. 197 | let one = t!(mk_header(tmppath.as_path(), false)); 198 | thread::sleep(time::Duration::from_millis(1050)); 199 | let two = t!(mk_header(tmppath.as_path(), true)); 200 | 201 | // Always expected to match. 202 | assert_eq!(t!(one.size()), t!(two.size())); 203 | assert_eq!(t!(one.path()), t!(two.path())); 204 | assert_eq!(t!(one.mode()), t!(two.mode())); 205 | 206 | // Would not match without `Deterministic`. 207 | assert_eq!(t!(one.mtime()), t!(two.mtime())); 208 | // TODO: No great way to validate that these would not be filled, but 209 | // check them anyway. 210 | assert_eq!(t!(one.uid()), t!(two.uid())); 211 | assert_eq!(t!(one.gid()), t!(two.gid())); 212 | } 213 | 214 | #[test] 215 | fn extended_numeric_format() { 216 | let mut h: GnuHeader = unsafe { mem::zeroed() }; 217 | h.as_header_mut().set_size(42); 218 | assert_eq!(h.size, [48, 48, 48, 48, 48, 48, 48, 48, 48, 53, 50, 0]); 219 | h.as_header_mut().set_size(8_589_934_593); 220 | assert_eq!(h.size, [0x80, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0, 1]); 221 | h.size = [0x80, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0, 0]; 222 | assert_eq!(h.as_header().entry_size().unwrap(), 0x0002_0000_0000); 223 | h.size = [48, 48, 48, 48, 48, 48, 48, 48, 48, 53, 51, 0]; 224 | assert_eq!(h.as_header().entry_size().unwrap(), 43); 225 | 226 | h.as_header_mut().set_gid(42); 227 | assert_eq!(h.gid, [48, 48, 48, 48, 48, 53, 50, 0]); 228 | assert_eq!(h.as_header().gid().unwrap(), 42); 229 | h.as_header_mut().set_gid(0x7fff_ffff_ffff_ffff); 230 | assert_eq!(h.gid, [0xff; 8]); 231 | assert_eq!(h.as_header().gid().unwrap(), 0x7fff_ffff_ffff_ffff); 232 | h.uid = [0x80, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78]; 233 | assert_eq!(h.as_header().uid().unwrap(), 0x1234_5678); 234 | 235 | h.mtime = [ 236 | 0x80, 0, 0, 0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 237 | ]; 238 | assert_eq!(h.as_header().mtime().unwrap(), 0x0123_4567_89ab_cdef); 239 | } 240 | 241 | #[test] 242 | fn byte_slice_conversion() { 243 | let h = Header::new_gnu(); 244 | let b: &[u8] = h.as_bytes(); 245 | let b_conv: &[u8] = Header::from_byte_slice(h.as_bytes()).as_bytes(); 246 | assert_eq!(b, b_conv); 247 | } 248 | --------------------------------------------------------------------------------