diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9ab178d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: [ "version-update:semver-patch" ] + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + open-pull-requests-limit: 10 diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 30bddfa..7a9a129 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -1,11 +1,11 @@ name: QA -on: [ push, pull_request ] +on: [ push, pull_request, merge_group ] jobs: spellcheck: name: Spellcheck - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 # Executes "typos ." diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6d83c74..4be95ef 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,13 +1,13 @@ name: Build -on: [push, pull_request] +on: [ push, pull_request, merge_group ] env: CARGO_TERM_COLOR: always jobs: build: - runs-on: ${{ matrix.runs-on }} + runs-on: "${{ matrix.runs-on }}" strategy: matrix: runs-on: @@ -16,16 +16,16 @@ jobs: rust: - stable - nightly - - 1.63.0 # MSVR + - 1.76.0 # MSVR steps: - uses: actions/checkout@v2 - # Important preparation step: override the latest default Rust version in GitHub CI - # with the current value of the iteration in the "strategy.matrix.rust"-array. - - uses: actions-rs/toolchain@v1 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable with: - profile: default - toolchain: ${{ matrix.rust }} - override: true + toolchain: "${{ matrix.rust }}" + - uses: Swatinem/rust-cache@v2 + with: + key: "${{ matrix.runs-on }}-${{ matrix.rust }}" - name: Build run: cargo build --all-targets --verbose --features alloc # use some arbitrary no_std target @@ -41,16 +41,16 @@ jobs: strategy: matrix: rust: - - 1.63.0 + - stable steps: - uses: actions/checkout@v2 - # Important preparation step: override the latest default Rust version in GitHub CI - # with the current value of the iteration in the "strategy.matrix.rust"-array. - - uses: actions-rs/toolchain@v1 + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: "${{ matrix.rust }}" + - uses: Swatinem/rust-cache@v2 with: - profile: default - toolchain: ${{ matrix.rust }} - override: true + key: "${{ matrix.runs-on }}-${{ matrix.rust }}" - name: Rustfmt run: cargo fmt -- --check - name: Clippy diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db0ec1..a0443f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# v0.3.0 (2024-05-03) +- MSRV is now 1.76 stable +- added support for more Tar archives + - 256 character long filename support (prefix + name) + - add support for space terminated numbers + - non-null terminated names + - iterate over directories: read regular files from directories + - more info: +- `TarArchive[Ref]::new` now returns a result +- added `unstable` feature with enhanced functionality for `nightly` compilers + - error types implement `core::error::Error` + # v0.2.0 (2023-04-11) - MSRV is 1.60.0 - bitflags bump: 1.x -> 2.x diff --git a/Cargo.toml b/Cargo.toml index 8507748..14c1cf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,11 @@ name = "tar-no-std" description = """ Library to read Tar archives (by GNU Tar) in `no_std` contexts with zero allocations. The crate is simple and only supports reading of "basic" archives, therefore no extensions, such -as GNU Longname. The maximum supported file name length is 256 characters excluding the NULL-byte -(using the tar name/prefix longname implementation).The maximum supported file size is 8GiB. +as GNU Longname. The maximum supported file name length is 256 characters excluding the NULL-byte +(using the tar name/prefix longname implementation).The maximum supported file size is 8GiB. Directories are supported, but only regular fields are yielded in iteration. """ -version = "0.2.0" +version = "0.3.0" edition = "2021" keywords = ["tar", "tarball", "archive"] categories = ["data-structures", "no-std", "parser-implementations"] @@ -16,6 +16,7 @@ license = "MIT" homepage = "https://github.com/phip1611/tar-no-std" repository = "https://github.com/phip1611/tar-no-std" documentation = "https://docs.rs/tar-no-std" +rust-version = "1.76.0" # required because "env_logger" uses "log" but with dependency to std.. resolver = "2" @@ -23,16 +24,20 @@ resolver = "2" [features] default = [] alloc = [] +unstable = [] # requires nightly [[example]] name = "alloc_feature" required-features = ["alloc"] [dependencies] -bitflags = "2.0" +bitflags = "2.5" log = { version = "0.4", default-features = false } -memchr = { version = "2.6.3", default-features = false } -num-traits = { version = "0.2.16", default-features = false } +memchr = { version = "2.7", default-features = false } +num-traits = { version = "~0.2", default-features = false } [dev-dependencies] -env_logger = "0.10" \ No newline at end of file +env_logger = "0.11" + +[package.metadata.docs.rs] +all-features = true diff --git a/README.md b/README.md index 79d1b99..eb75e5f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # `tar-no-std` - Parse Tar Archives (Tarballs) _Due to historical reasons, there are several formats of tar archives. All of them are based on the same principles, -but have some subtle differences that often make them incompatible with each other._ [[0]] +but have some subtle differences that often make them incompatible with each other._ [0] Library to read Tar archives (by GNU Tar) in `no_std` contexts with zero allocations. If you have a standard environment and need full feature support, I recommend the use of instead. @@ -52,7 +52,8 @@ If your tar file is compressed, e.g. by `.tar.gz`/`gzip`, you need to uncompress bytes. ## MSRV -The MSRV is 1.52.1 stable. +The MSRV is 1.76.0 stable. -[0]: https://www.gnu.org/software/tar/manual/html_section/Formats.html +## References +[0]\: https://www.gnu.org/software/tar/manual/html_section/Formats.html diff --git a/examples/alloc_feature.rs b/examples/alloc_feature.rs index 7ac7f75..a631c3e 100644 --- a/examples/alloc_feature.rs +++ b/examples/alloc_feature.rs @@ -32,7 +32,7 @@ fn main() { // also works in no_std environment (except the println!, of course) let archive = include_bytes!("../tests/gnu_tar_default.tar"); let archive_heap_owned = archive.to_vec().into_boxed_slice(); - let archive = TarArchive::new(archive_heap_owned); + let archive = TarArchive::new(archive_heap_owned).unwrap(); // Vec needs an allocator of course, but the library itself doesn't need one let entries = archive.entries().collect::>(); println!("{:#?}", entries); diff --git a/examples/minimal.rs b/examples/minimal.rs index e16fe81..e0140d5 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -30,7 +30,7 @@ fn main() { // also works in no_std environment (except the println!, of course) let archive = include_bytes!("../tests/gnu_tar_default.tar"); - let archive = TarArchiveRef::new(archive); + let archive = TarArchiveRef::new(archive).unwrap(); // Vec needs an allocator of course, but the library itself doesn't need one let entries = archive.entries().collect::>(); println!("{:#?}", entries); diff --git a/src/archive.rs b/src/archive.rs index ae7a889..f809c2e 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -29,7 +29,7 @@ use crate::tar_format_types::TarFormatString; use crate::{TypeFlag, BLOCKSIZE, POSIX_1003_MAX_FILENAME_LEN}; #[cfg(feature = "alloc")] use alloc::boxed::Box; -use core::fmt::{Debug, Formatter}; +use core::fmt::{Debug, Display, Formatter}; use core::str::Utf8Error; use log::warn; @@ -84,13 +84,29 @@ impl<'a> Debug for ArchiveEntry<'a> { } } +/// The data is corrupt and doesn't present a valid Tar archive. Reasons for +/// that are: +/// - the data is empty +/// - the data is not a multiple of 512 (the BLOCKSIZE) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct CorruptDataError; + +impl Display for CorruptDataError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Debug::fmt(self, f) + } +} + +#[cfg(feature = "unstable")] +impl core::error::Error for CorruptDataError {} + /// Type that owns bytes on the heap, that represents a Tar archive. /// Unlike [`TarArchiveRef`], this type is useful, if you need to own the -/// data as long as you need the archive, but not longer. +/// data as long as you need the archive, but no longer. /// /// This is only available with the `alloc` feature of this crate. #[cfg(feature = "alloc")] -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TarArchive { data: Box<[u8]>, } @@ -99,15 +115,11 @@ pub struct TarArchive { impl TarArchive { /// Creates a new archive type, that owns the data on the heap. The provided byte array is /// interpreted as bytes in Tar archive format. - pub fn new(data: Box<[u8]>) -> Self { - assert_eq!( - data.len() % BLOCKSIZE, - 0, - "data must be a multiple of BLOCKSIZE={}, len is {}", - BLOCKSIZE, - data.len(), - ); - Self { data } + pub fn new(data: Box<[u8]>) -> Result { + let is_malformed = (data.len() % BLOCKSIZE) != 0; + (!data.is_empty() && !is_malformed) + .then_some(Self { data }) + .ok_or(CorruptDataError) } /// Iterates over all entries of the Tar archive. @@ -121,7 +133,7 @@ impl TarArchive { #[cfg(feature = "alloc")] impl From> for TarArchive { fn from(data: Box<[u8]>) -> Self { - Self::new(data) + Self::new(data).unwrap() } } @@ -134,23 +146,20 @@ impl From for Box<[u8]> { /// Wrapper type around bytes, which represents a Tar archive. /// Unlike [`TarArchive`], this uses only a reference to the data. -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TarArchiveRef<'a> { data: &'a [u8], } #[allow(unused)] impl<'a> TarArchiveRef<'a> { - /// Creates a new archive wrapper type. The provided byte array is interpreted as - /// bytes in Tar archive format. - pub fn new(data: &'a [u8]) -> Self { - assert_eq!( - data.len() % BLOCKSIZE, - 0, - "data must be a multiple of BLOCKSIZE={}", - BLOCKSIZE - ); - Self { data } + /// Creates a new archive wrapper type. The provided byte array is + /// interpreted as bytes in Tar archive format. + pub fn new(data: &'a [u8]) -> Result { + let is_malformed = (data.len() % BLOCKSIZE) != 0; + (!data.is_empty() && !is_malformed) + .then_some(Self { data }) + .ok_or(CorruptDataError) } /// Iterates over all entries of the Tar archive. @@ -278,24 +287,39 @@ mod tests { use super::*; use std::vec::Vec; + #[test] + #[rustfmt::skip] + fn test_constructor_returns_error() { + assert_eq!(TarArchiveRef::new(&[0]), Err(CorruptDataError)); + assert_eq!(TarArchiveRef::new(&[]), Err(CorruptDataError)); + assert!(TarArchiveRef::new(&[0; BLOCKSIZE]).is_ok()); + + #[cfg(feature = "alloc")] + { + assert_eq!(TarArchive::new(vec![].into_boxed_slice()), Err(CorruptDataError)); + assert_eq!(TarArchive::new(vec![0].into_boxed_slice()), Err(CorruptDataError)); + assert!(TarArchive::new(vec![0; BLOCKSIZE].into_boxed_slice()).is_ok()); + }; + } + #[test] fn test_archive_list() { - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar")); + let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar")).unwrap(); let entries = archive.entries().collect::>(); println!("{:#?}", entries); } /// Tests to read the entries from existing archives in various Tar flavors. #[test] fn test_archive_entries() { - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar")); + let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_archive_content(&entries); - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_gnu.tar")); + let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_gnu.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_archive_content(&entries); - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_oldgnu.tar")); + let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_oldgnu.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_archive_content(&entries); @@ -309,11 +333,11 @@ mod tests { let entries = archive.entries().collect::>(); assert_archive_content(&entries);*/ - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar.tar")); + let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_archive_content(&entries); - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_v7.tar")); + let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_v7.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_archive_content(&entries); } @@ -323,7 +347,8 @@ mod tests { fn test_archive_with_long_dir_entries() { // tarball created with: // $ cd tests; gtar --format=ustar -cf gnu_tar_ustar_long.tar 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234/ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar_long.tar")); + let archive = + TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar_long.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_eq!(entries.len(), 2); @@ -337,7 +362,8 @@ mod tests { fn test_archive_with_deep_dir_entries() { // tarball created with: // $ cd tests; gtar --format=ustar -cf gnu_tar_ustar_deep.tar 0123456789 - let archive = TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar_deep.tar")); + let archive = + TarArchiveRef::new(include_bytes!("../tests/gnu_tar_ustar_deep.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_eq!(entries.len(), 1); @@ -350,7 +376,8 @@ mod tests { // $ gtar -cf tests/gnu_tar_default_with_dir.tar --exclude '*.tar' --exclude '012345678*' tests { let archive = - TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default_with_dir.tar")); + TarArchiveRef::new(include_bytes!("../tests/gnu_tar_default_with_dir.tar")) + .unwrap(); let entries = archive.entries().collect::>(); assert_archive_with_dir_content(&entries); @@ -359,7 +386,8 @@ mod tests { // tarball created with: // $(osx) tar -cf tests/mac_tar_ustar_with_dir.tar --format=ustar --exclude '*.tar' --exclude '012345678*' tests { - let archive = TarArchiveRef::new(include_bytes!("../tests/mac_tar_ustar_with_dir.tar")); + let archive = + TarArchiveRef::new(include_bytes!("../tests/mac_tar_ustar_with_dir.tar")).unwrap(); let entries = archive.entries().collect::>(); assert_archive_with_dir_content(&entries); @@ -373,7 +401,7 @@ mod tests { let data = include_bytes!("../tests/gnu_tar_default.tar") .to_vec() .into_boxed_slice(); - let archive = TarArchive::new(data.clone()); + let archive = TarArchive::new(data.clone()).unwrap(); let entries = archive.entries().collect::>(); assert_archive_content(&entries); diff --git a/src/lib.rs b/src/lib.rs index c7f8caa..691fe64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ SOFTWARE. //! //! // also works in no_std environment (except the println!, of course) //! let archive = include_bytes!("../tests/gnu_tar_default.tar"); -//! let archive = TarArchiveRef::new(archive); +//! let archive = TarArchiveRef::new(archive).unwrap(); //! // Vec needs an allocator of course, but the library itself doesn't need one //! let entries = archive.entries().collect::>(); //! println!("{:#?}", entries); @@ -62,6 +62,7 @@ SOFTWARE. //! println!("{:#?}", last_file_content); //! ``` +#![cfg_attr(feature = "unstable", feature(error_in_core))] #![cfg_attr(not(test), no_std)] #![deny( clippy::all, diff --git a/src/tar_format_types.rs b/src/tar_format_types.rs index 8b7ed4a..e58b8a5 100644 --- a/src/tar_format_types.rs +++ b/src/tar_format_types.rs @@ -22,7 +22,7 @@ pub struct TarFormatString { /// This string will be null terminated if it doesn't fill the entire array. impl TarFormatString { /// Constructor. - pub fn new(bytes: [u8; N]) -> Self { + pub const fn new(bytes: [u8; N]) -> Self { if N == 0 { panic!("Array cannot be zero length"); } @@ -194,6 +194,7 @@ mod tests { } #[test] + #[allow(clippy::cognitive_complexity)] fn test_append() { let mut s = TarFormatString::new([0; 20]);