summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml3
-rw-r--r--README.md27
-rw-r--r--examples/alloc_correct.rs2
-rw-r--r--examples/basic.rs (renamed from examples/very_basic.rs)2
-rwxr-xr-xpre-commit3
-rw-r--r--src/lib.rs124
6 files changed, 137 insertions, 24 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 28afab4..37e7c99 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,5 +9,6 @@ edition = "2021"
png = "0.17.10"
[features]
-std = []
default = ["std"]
+std = []
+adler = []
diff --git a/README.md b/README.md
index 04e126a..c50e4da 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Tiny Rust PNG decoder.
-This decoder can be used without `std` or even `alloc` by disabling the `std` feature (enabled by default).
+This decoder can be used without `std` or `alloc` by disabling the `std` feature (enabled by default).
## Goals
@@ -15,17 +15,19 @@ This decoder can be used without `std` or even `alloc` by disabling the `std` fe
- Adam7 interlacing (interlaced PNGs are rare, and this would require additional code complexity
— but if you want to add it behind a feature gate, feel free to)
-- Sacrificing code size for speed (except maybe with a feature enabled)
+- Sacrificing code size a lot for speed (except maybe with a feature enabled)
+- Checking block CRCs (increased code complexity probably isn't worth it,
+ and there's already Adler32 checksums for IDAT chunks)
## Example usage
-Very basic usage:
+Basic usage:
```rust
let mut buffer = vec![0; 1 << 20]; // hope this is big enough!
let mut png = &include_bytes!("image.png")[..];
let image = tiny_png::read_png(&mut png, None, &mut buffer).expect("bad PNG");
-println!("{}x{} image", image.width(), image.height());
+println!("{}×{} image", image.width(), image.height());
let pixels = image.pixels();
println!("top-left pixel is #{:02x}{:02x}{:02x}", pixels[0], pixels[1], pixels[2]);
// (^ this only makes sense for RGB 8bpc images)
@@ -39,8 +41,23 @@ let header = tiny_png::read_png_header(&mut png).expect("bad PNG");
println!("need {} bytes of memory", header.required_bytes());
let mut buffer = vec![0; header.required_bytes()];
let image = tiny_png::read_png(&mut png, Some(&header), &mut buffer).expect("bad PNG");
-println!("{}x{} image", image.width(), image.height());
+println!("{}×{} image", image.width(), image.height());
let pixels = image.pixels();
println!("top-left pixel is #{:02x}{:02x}{:02x}", pixels[0], pixels[1], pixels[2]);
// (^ this only makes sense for RGB 8bpc images)
```
+
+## Features
+
+- `std` (default: enabled) — use standard library. enabling it allows you to read from `BufReader<File>` but
+ adds `std` as a dependency.
+- `adler` (default: disabled) — check Adler-32 checksums. slightly increases code size and
+ slightly decreases performance but verifies integrity of PNG files.
+
+## Development
+
+A `pre-commit` git hook is provided to run `cargo fmt` and `cargo clippy`. You can install it with:
+
+```sh
+ln -s ../../pre-commit .git/hooks/
+```
diff --git a/examples/alloc_correct.rs b/examples/alloc_correct.rs
index 6711a1c..7be3a2c 100644
--- a/examples/alloc_correct.rs
+++ b/examples/alloc_correct.rs
@@ -4,7 +4,7 @@ fn main() {
println!("need {} bytes of memory", header.required_bytes());
let mut buffer = vec![0; header.required_bytes()];
let image = tiny_png::read_png(&mut png, Some(&header), &mut buffer).expect("bad PNG");
- println!("{}x{} image", image.width(), image.height());
+ println!("{}×{} image", image.width(), image.height());
let pixels = image.pixels();
println!(
"top-left pixel is #{:02x}{:02x}{:02x}",
diff --git a/examples/very_basic.rs b/examples/basic.rs
index 33043b6..90b476a 100644
--- a/examples/very_basic.rs
+++ b/examples/basic.rs
@@ -2,7 +2,7 @@ fn main() {
let mut my_buffer = vec![0; 1 << 20]; // hope this is big enough!
let mut png = &include_bytes!("image.png")[..];
let image = tiny_png::read_png(&mut png, None, &mut my_buffer).expect("bad PNG");
- println!("{}x{} image", image.width(), image.height());
+ println!("{}×{} image", image.width(), image.height());
let pixels = image.pixels();
println!(
"top-left pixel is #{:02x}{:02x}{:02x}",
diff --git a/pre-commit b/pre-commit
new file mode 100755
index 0000000..8dcd951
--- /dev/null
+++ b/pre-commit
@@ -0,0 +1,3 @@
+cargo fmt || exit 1
+cargo clippy -- -D warnings || exit 1
+git add -u src
diff --git a/src/lib.rs b/src/lib.rs
index 1602bca..855aaa8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,6 +10,7 @@ impl<T: Sized + Display + Debug> IOError for T {}
#[non_exhaustive]
pub enum Error<I: IOError> {
IO(I),
+ BufferTooSmall,
NotPng,
BadIhdr,
UnrecognizedChunk([u8; 4]),
@@ -27,6 +28,7 @@ pub enum Error<I: IOError> {
BadTrnsChunk,
BadNlen,
NoIdat,
+ BadAdlerChecksum,
}
impl<I: IOError> From<I> for Error<I> {
@@ -41,6 +43,7 @@ impl<I: IOError> Display for Error<I> {
Self::IO(e) => write!(f, "{e}"),
Self::NotPng => write!(f, "not a png file"),
Self::BadIhdr => write!(f, "bad IHDR chunk"),
+ Self::BufferTooSmall => write!(f, "provided buffer is too small"),
Self::UnrecognizedChunk([a, b, c, d]) => {
write!(f, "unrecognized chunk type: {a} {b} {c} {d}")
}
@@ -60,6 +63,7 @@ impl<I: IOError> Display for Error<I> {
Self::BadTrnsChunk => write!(f, "bad tRNS chunk"),
Self::NoIdat => write!(f, "missing IDAT chunk"),
Self::BadNlen => write!(f, "LEN doesn't match NLEN"),
+ Self::BadAdlerChecksum => write!(f, "bad adler-32 checksum"),
}
}
}
@@ -131,6 +135,7 @@ struct IdatReader<'a, R: Read> {
bytes_left_in_block: usize,
palette: &'a mut [[u8; 4]; 256],
header: &'a ImageHeader,
+ eof: bool,
}
impl<R: Read> IdatReader<'_, R> {
@@ -147,8 +152,13 @@ impl<R: Read> IdatReader<'_, R> {
// CRC
self.inner.skip_bytes(4)?;
+
match read_non_idat_chunks(self.inner, self.header, self.palette)? {
- None => Ok(bytes_read),
+ None => {
+ self.bytes_left_in_block = 0;
+ self.eof = true;
+ Ok(bytes_read)
+ }
Some(n) => {
self.bytes_left_in_block = n;
Ok(self.read_partial(&mut buf[bytes_read..])? + bytes_read)
@@ -167,15 +177,20 @@ impl<R: Read> IdatReader<'_, R> {
}
fn read_to_end(&mut self) -> Result<(), Error<R::Error>> {
- self.inner.skip_bytes(self.bytes_left_in_block)?;
- // CRC
- self.inner.skip_bytes(4)?;
- loop {
- match read_non_idat_chunks(self.inner, self.header, self.palette)? {
- None => break,
- Some(n) => self.inner.skip_bytes(n + 4)?,
+ if !self.eof {
+ if self.bytes_left_in_block > 0 {
+ self.inner.skip_bytes(self.bytes_left_in_block)?;
+ }
+ // CRC
+ self.inner.skip_bytes(4)?;
+ loop {
+ match read_non_idat_chunks(self.inner, self.header, self.palette)? {
+ None => break,
+ Some(n) => self.inner.skip_bytes(n + 4)?,
+ }
}
}
+ self.eof = true;
Ok(())
}
}
@@ -594,6 +609,8 @@ fn read_compressed_block<R: Read>(
i += 1;
}
} else {
+ // since we only assigned 0..=18 in the huffman table,
+ // we should never get a value outside that range.
debug_assert!(false, "should not be reachable");
}
if i >= total_code_lengths {
@@ -643,11 +660,11 @@ fn read_compressed_block<R: Read>(
let length = match literal_length {
257..=264 => literal_length - 254,
265..=284 => {
- const BASES: [u16; 20] = [
+ const BASES: [u8; 20] = [
11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131,
163, 195, 227,
];
- let base = BASES[usize::from(literal_length - 265)];
+ let base: u16 = BASES[usize::from(literal_length - 265)].into();
let extra_bits = (literal_length - 261) as u8 / 4;
let extra = reader.read_bits_u16(extra_bits)?;
base + extra
@@ -683,7 +700,22 @@ fn read_idat<R: Read>(
writer: &mut DecompressedDataWriter,
) -> Result<(), Error<R::Error>> {
let mut reader = BitReader::from(reader);
- let _zlib_header = reader.read_bits(16);
+ // zlib header
+ let cmf = reader.read_bits(8)?;
+ let flags = reader.read_bits(8)?;
+ // check zlib checksum
+ if (cmf * 256 + flags) % 31 != 0 {
+ return Err(Error::BadZlibHeader);
+ }
+ let compression_method = cmf & 0xf;
+ let compression_info = cmf >> 4;
+ if compression_method != 8 || compression_info > 7 {
+ return Err(Error::BadZlibHeader);
+ }
+ if (flags & 0x100) != 0 {
+ return Err(Error::BadZlibHeader);
+ }
+
let decompressed_size = reader.inner.header.decompressed_size();
while writer.pos < decompressed_size {
let bfinal = reader.read_bits(1)?;
@@ -714,6 +746,40 @@ fn read_idat<R: Read>(
break;
}
}
+
+ #[cfg(feature = "adler")]
+ {
+ // adler32 checksum
+ let padding = reader.bits_left % 8;
+ if padding > 0 {
+ reader.bits >>= padding;
+ reader.bits_left -= padding;
+ }
+ // NOTE: currently `read_bits` doesn't support reads of 32 bits.
+ let mut expected_adler = reader.read_bits(16)?;
+ expected_adler |= reader.read_bits(16)? << 16;
+ expected_adler = expected_adler.swap_bytes();
+
+ const BASE: u32 = 65521;
+ let mut s1: u32 = 1;
+ let mut s2: u32 = 0;
+ for byte in writer.slice[..decompressed_size].iter().copied() {
+ s1 += u32::from(byte);
+ if s1 > BASE {
+ s1 -= BASE;
+ }
+ s2 += s1;
+ if s2 > BASE {
+ s2 -= BASE;
+ }
+ }
+ let got_adler = s2 << 16 | s1;
+ if got_adler != expected_adler {
+ return Err(Error::BadAdlerChecksum);
+ }
+ }
+
+ // padding bytes
reader.inner.read_to_end()?;
Ok(())
@@ -878,6 +944,9 @@ pub fn read_png<'a, R: Read>(
None => read_png_header(reader)?,
Some(h) => *h,
};
+ if buf.len() < header.required_bytes() {
+ return Err(Error::BufferTooSmall);
+ }
let mut writer = DecompressedDataWriter::from(buf);
let mut palette = [[0, 0, 0, 0]; 256];
let Some(idat_len) = read_non_idat_chunks(reader, &header, &mut palette)? else {
@@ -889,6 +958,7 @@ pub fn read_png<'a, R: Read>(
bytes_left_in_block: idat_len,
header: &header,
palette: &mut palette,
+ eof: false,
},
&mut writer,
)?;
@@ -905,19 +975,22 @@ pub fn read_png<'a, R: Read>(
#[cfg(test)]
mod tests {
use super::*;
+ #[cfg(feature = "std")]
use std::fs::File;
+ extern crate alloc;
+ #[cfg(feature = "std")]
fn test_file(path: &str) {
let decoder = png::Decoder::new(File::open(path).expect("file not found"));
let mut reader = decoder.read_info().unwrap();
- let mut png_buf = vec![0; reader.output_buffer_size()];
+ let mut png_buf = alloc::vec![0; reader.output_buffer_size()];
let png_header = reader.next_frame(&mut png_buf).unwrap();
let png_bytes = &png_buf[..png_header.buffer_size()];
let mut r = std::io::BufReader::new(File::open(path).expect("file not found"));
let tiny_header = read_png_header(&mut r).unwrap();
- let mut tiny_buf = vec![0; tiny_header.required_bytes()];
+ let mut tiny_buf = alloc::vec![0; tiny_header.required_bytes()];
let image = read_png(&mut r, Some(&tiny_header), &mut tiny_buf).unwrap();
let tiny_bytes = image.pixels();
@@ -929,12 +1002,12 @@ mod tests {
let decoder = png::Decoder::new(bytes);
let mut reader = decoder.read_info().unwrap();
- let mut png_buf = vec![0; reader.output_buffer_size()];
+ let mut png_buf = alloc::vec![0; reader.output_buffer_size()];
let png_header = reader.next_frame(&mut png_buf).unwrap();
let png_bytes = &png_buf[..png_header.buffer_size()];
let tiny_header = read_png_header(&mut bytes).unwrap();
- let mut tiny_buf = vec![0; tiny_header.required_bytes()];
+ let mut tiny_buf = alloc::vec![0; tiny_header.required_bytes()];
let image = read_png(&mut bytes, Some(&tiny_header), &mut tiny_buf).unwrap();
let tiny_bytes = image.pixels();
@@ -944,7 +1017,10 @@ mod tests {
macro_rules! test_both {
($file:literal) => {
- test_file($file);
+ #[cfg(feature = "std")]
+ {
+ test_file($file);
+ }
test_bytes(include_bytes!(concat!("../", $file)));
};
}
@@ -1041,4 +1117,20 @@ mod tests {
fn test_ouroboros() {
test_both!("test/ouroboros.png");
}
+ #[test]
+ fn test_bad_png() {
+ let mut data = &b"hello"[..];
+ // in this case we might actually get an unexpected EOF
+ assert!(read_png_header(&mut data).is_err());
+ let mut data = &b"helloadfalskdfjlksajdflkjsadlkfj"[..];
+ let err = read_png_header(&mut data).unwrap_err();
+ assert!(matches!(err, Error::NotPng));
+ }
+ #[test]
+ fn test_buffer_too_small() {
+ let mut data = &include_bytes!("../test/ouroboros.png")[..];
+ let mut buffer = [0; 128];
+ let err = read_png(&mut data, None, &mut buffer[..]).unwrap_err();
+ assert!(matches!(err, Error::BufferTooSmall));
+ }
}