diff --git a/.github/ISSUE_TEMPLATE/1-problem.md b/.github/ISSUE_TEMPLATE/1-problem.md new file mode 100644 index 0000000..cc4f015 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-problem.md @@ -0,0 +1,7 @@ +--- +name: Problem +about: Something does not seem right + +--- + + diff --git a/.github/ISSUE_TEMPLATE/2-suggestion.md b/.github/ISSUE_TEMPLATE/2-suggestion.md new file mode 100644 index 0000000..5c4aed2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-suggestion.md @@ -0,0 +1,7 @@ +--- +name: Suggestion +about: Share how Serde could support your use case better + +--- + + diff --git a/.github/ISSUE_TEMPLATE/3-documentation.md b/.github/ISSUE_TEMPLATE/3-documentation.md new file mode 100644 index 0000000..5a83985 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-documentation.md @@ -0,0 +1,7 @@ +--- +name: Documentation +about: Certainly there is room for improvement + +--- + + diff --git a/.github/ISSUE_TEMPLATE/4-other.md b/.github/ISSUE_TEMPLATE/4-other.md new file mode 100644 index 0000000..0127fea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-other.md @@ -0,0 +1,7 @@ +--- +name: Anything else! +about: Whatever is on your mind + +--- + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c1dc146 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + schedule: [cron: "40 1 * * *"] + +permissions: + contents: read + +env: + RUSTFLAGS: -Dwarnings + +jobs: + test: + name: Test suite + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: cargo test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: Cargo.lock + path: Cargo.lock + + stable: + name: Rust ${{matrix.rust}} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: [stable, beta] + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{matrix.rust}} + - run: cargo build + + nightly: + name: Rust nightly ${{matrix.os == 'windows' && '(windows)' || ''}} + runs-on: ${{matrix.os}}-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu] + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: cargo build + + doc: + name: Documentation + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + RUSTDOCFLAGS: -Dwarnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/install@cargo-docs-rs + - run: cargo docs-rs + + clippy: + name: Clippy + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@clippy + - run: cargo clippy -- -Dclippy::all -Dclippy::pedantic + + # miri: + # name: Miri + # runs-on: ubuntu-latest + # timeout-minutes: 45 + # steps: + # - uses: actions/checkout@v4 + # - uses: dtolnay/rust-toolchain@miri + # - run: cargo miri setup + # - run: cargo miri test diff --git a/examples/read_limited.rs b/examples/read_limited.rs index 3fa911f..63a4152 100644 --- a/examples/read_limited.rs +++ b/examples/read_limited.rs @@ -15,7 +15,7 @@ fn main() -> LimitReaderResult<()> { ); let data = limit_reader.buffer(); - let text = String::from_utf8(data[..bytes_read].to_vec())?; + let text = String::from_utf8(data[..(bytes_read as usize)].to_vec())?; println!("First line from README: {}", &text); diff --git a/src/error.rs b/src/error.rs index d5b9341..51e90bd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,13 +2,16 @@ use crate::{error_from, LimitReaderOutputBuilderError}; use std::{ error::Error as StdError, fmt::{self}, + num::TryFromIntError, string::FromUtf8Error, }; /// Boxed error, a ptr to the Error via dynamic dispatch allocated on the heap at run time. +#[allow(clippy::module_name_repetitions)] pub type BoxError = Box; /// Default error type for create. +#[allow(clippy::module_name_repetitions)] pub type LimitReaderError = Error; /// Error type @@ -19,20 +22,27 @@ pub struct Error { #[derive(Debug)] #[non_exhaustive] +#[allow(clippy::module_name_repetitions)] +#[allow(clippy::enum_variant_names)] pub enum ErrorKind { IoError, - Utf8Error, LimitReaderOutputBuilderError, + Utf8Error, + TryFromIntError, + LimitReaderError(Box), } impl ErrorKind { pub(crate) fn as_str(&self) -> &'static str { + #[allow(clippy::enum_glob_use)] use ErrorKind::*; // tidy-alphabetical-start match *self { IoError => "io error", Utf8Error => "invalid utf-8", LimitReaderOutputBuilderError => "builder error", + TryFromIntError => "conversion error", + LimitReaderError(_) => "boxed error", } } } @@ -78,6 +88,7 @@ error_from!( LimitReaderOutputBuilderError, ErrorKind::LimitReaderOutputBuilderError ); +error_from!(TryFromIntError, ErrorKind::TryFromIntError); #[macro_use] pub mod macros { diff --git a/src/lib.rs b/src/lib.rs index 36e5671..9040ef4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! //! Exposes [`LimitReader`] which is a limit reader, that protects against zip-bombs and other nefarious activities. //! -//! This crate is heavily inspired by Jon Gjengset's "Crust of Rust" episode on the inner workings of git on YouTube () and mitigrating Zip-bombs. +//! This crate is heavily inspired by Jon Gjengset's "Crust of Rust" episode on the inner workings of git on `YouTube` () and mitigrating Zip-bombs. use derive_builder::Builder; use error::LimitReaderError; @@ -27,17 +27,17 @@ pub(crate) mod readable; /// Default result type for [`LimitReader`] pub type LimitReaderResult = std::result::Result; -/// Re-exports +/// Re-exports Traits and macros used by most projects. Add `use better_limit_reader::prelude::*;` to your code to quickly get started with [`LimitReader`]. pub mod prelude { - //! Traits and macros used by most projects. Add `use better_limit_reader::prelude::*;` to your code to quickly get started with LimitReader. + pub use crate::{error::LimitReaderError, LimitReader, LimitReaderOutput, LimitReaderResult}; } #[allow(dead_code)] -/// The [LimitReader] reads into `buf` which is held within the record struct. +/// The [`LimitReader`] reads into `buf` which is held within the record struct. pub struct LimitReader { buf: [u8; Self::DEFAULT_BUF_SIZE], - expected_size: usize, + expected_size: u64, decode_zlib: bool, decode_gzip: bool, } @@ -54,22 +54,24 @@ impl LimitReader { pub const DEFAULT_BUF_SIZE: usize = 1024; /// Create a new [`LimitReader`] with a [`LimitReader::DEFAULT_BUF_SIZE`] for the limit-readers max threshold. + #[must_use] pub fn new() -> Self { Self { buf: [0; Self::DEFAULT_BUF_SIZE], - expected_size: Self::DEFAULT_BUF_SIZE - 1, + expected_size: (Self::DEFAULT_BUF_SIZE - 1) as u64, decode_zlib: false, decode_gzip: false, } } /// Return a reference to the internal buffer. + #[must_use] pub fn buffer(&self) -> &[u8; Self::DEFAULT_BUF_SIZE] { &self.buf } /// Increase the allowed limit on the [`LimitReader`] - pub fn limit(&mut self, limit: usize) -> &mut Self { + pub fn limit(&mut self, limit: u64) -> &mut Self { self.expected_size = limit; self @@ -86,13 +88,21 @@ impl LimitReader { // NOTE: This is private until this is implemented in the future. /// Enable decoding from compressed Gzip fn enable_decode_gzip(&mut self) -> &mut Self { - unimplemented!() - // self.decode_gzip = true; + self.decode_gzip = true; - // self + self } /// Read from provided source file. If the source data is already Zlib compressed, optionally decode the data stream before reading it through a limit-reader. + /// + /// # Panics + /// + /// If the provided source file does not exist or is inaccessible, it will panic. Refer to [`std::fs::File::open`] for details. This will return [`LimitReaderError`]. + /// + /// # Errors + /// + /// If this function encounters an error of the kind [`LimitReaderError`], this error will be returned. + /// pub fn read(&mut self, source: PathBuf) -> Result { let f = std::fs::File::open(source).expect("Unable to open file"); if self.decode_zlib { @@ -110,6 +120,11 @@ impl LimitReader { } /// Given an accessible source file, this will automatically limit the contents read to the size of the buffer itself. This will silently truncate read bytes into the buffer, without raising an error. + /// + /// # Errors + /// + /// If this function encounters an error of the kind [`LimitReaderError`], this error will be returned. + /// pub fn read_limited(&mut self, source: PathBuf) -> Result { let source_bytes = std::fs::metadata(&source)?.len(); let f = std::fs::File::open(source)?; @@ -153,18 +168,21 @@ pub struct LimitReaderOutput { impl LimitReaderOutput { /// Return bytes read by the underlying reader. - pub fn bytes_read(&self) -> usize { - self.bytes_read as usize + #[must_use] + pub fn bytes_read(&self) -> u64 { + self.bytes_read } /// Size in bytes of the underlying file accessible to the reader. - pub fn source_size(&self) -> usize { - self.source_size as usize + #[must_use] + pub fn source_size(&self) -> u64 { + self.source_size } /// Unread bytes (from the underlying file accessible to the reader). - pub fn bytes_remaining(&self) -> usize { - (self.source_size - self.bytes_read) as usize + #[must_use] + pub fn bytes_remaining(&self) -> u64 { + self.source_size - self.bytes_read } } @@ -243,12 +261,12 @@ mod tests { writeln!(file, "{}", &text).unwrap(); let mut limit_reader = LimitReader::new(); - let limit = 8_usize; + let limit = 8_u64; limit_reader.limit(limit); match limit_reader.read(file_path) { Ok(read_size) => { - assert!(read_size == limit); + assert!(read_size == limit.try_into().unwrap()); } Err(err) => { assert_eq!("Error: too many bytes", err.to_string()); @@ -325,7 +343,7 @@ mod tests { writeln!(file, "{}", &text).unwrap(); let mut limit_reader = LimitReader::new(); - let limit = 8_usize; + let limit = 8_u64; limit_reader.limit(limit); match limit_reader.read_limited(file_path.clone()) { @@ -350,10 +368,11 @@ mod tests { Ok(reader_output) => { let bytes_read = reader_output.bytes_read(); let persisted_text = - String::from_utf8(limit_reader.buf[..bytes_read].to_vec()).unwrap(); + String::from_utf8(limit_reader.buf[..(bytes_read as usize)].to_vec()) + .unwrap(); assert_eq!( persisted_text, - format!("{}", &text[..bytes_read]).to_string() + format!("{}", &text[..(bytes_read as usize)]).to_string() ); } Err(_) => unreachable!(), @@ -373,7 +392,7 @@ mod tests { writeln!(file, "{}", &text).unwrap(); let mut limit_reader = LimitReader::new(); - let limit = 8_usize; + let limit = 8_u64; limit_reader // RA block .limit(limit) diff --git a/src/readable.rs b/src/readable.rs index 486f16d..df81dfc 100644 --- a/src/readable.rs +++ b/src/readable.rs @@ -1,3 +1,4 @@ +#[allow(clippy::wildcard_imports)] use super::*; pub struct MyBufReader(pub Z); @@ -12,7 +13,11 @@ pub trait Readable { fn perform_read(&mut self, buf: &mut [u8]) -> io::Result; } +#[allow(dead_code)] +type ReaderResult = std::result::Result; + pub(crate) mod falible { + #[allow(clippy::wildcard_imports)] use super::*; impl Readable for LimitReaderFallible @@ -29,7 +34,7 @@ pub(crate) mod falible { R: Read, { reader: R, - limit: usize, + limit: u64, reader_count: usize, } @@ -37,7 +42,7 @@ pub(crate) mod falible { where R: Read, { - pub fn new(r: R, limit: usize) -> Self { + pub fn new(r: R, limit: u64) -> Self { Self { reader: r, limit, @@ -50,15 +55,16 @@ pub(crate) mod falible { where R: Read, { + #[allow(clippy::cast_possible_truncation)] fn read(&mut self, mut buf: &mut [u8]) -> io::Result { // NOTE: using +1 in the range below trips the error. - buf = &mut buf[..self.limit + 1]; + buf = &mut buf[..=(self.limit as usize)]; let bytes_read = self.reader.read(buf)?; - if bytes_read > self.limit { + if bytes_read > self.limit as usize { return Err(io::Error::new(io::ErrorKind::Other, "too many bytes")); } - self.limit -= bytes_read; + self.limit -= bytes_read as u64; self.reader_count += 1; Ok(bytes_read) @@ -78,6 +84,8 @@ pub(crate) mod falible { } pub(crate) mod infalible { + + #[allow(clippy::wildcard_imports)] use super::*; impl Readable for LimitReaderInfallible @@ -94,7 +102,7 @@ pub(crate) mod infalible { R: Read, { reader: R, - limit: usize, + limit: u64, reader_count: usize, } @@ -102,7 +110,7 @@ pub(crate) mod infalible { where R: Read, { - pub fn new(r: R, limit: usize) -> Self { + pub fn new(r: R, limit: u64) -> Self { Self { reader: r, limit, @@ -116,12 +124,22 @@ pub(crate) mod infalible { R: Read, { fn read(&mut self, buf: &mut [u8]) -> io::Result { - let max_read = self.limit.min(buf.len()); // min of limit and buf.len() - - let bytes_read = self.reader.read(&mut buf[..max_read])?; - self.reader_count += 1; - - Ok(bytes_read) + match TryInto::::try_into(buf.len()) { + Ok(buf_len) => { + let max_read = self.limit.min(buf_len); // min of limit and buf.len() + + match TryInto::::try_into(max_read) { + Ok(m) => { + let bytes_read = self.reader.read(&mut buf[..m])?; + self.reader_count += 1; + + Ok(bytes_read) + } + Err(_) => Ok(0), + } + } + Err(_) => Ok(0), + } } }