Quick Note: Validating PNG in Rust
So, I have been learning Rust for, say, 7 months or so. I kinda get the grasp of it but still not fluent at it.
I'd like to create a project on it but have been also thinking what kind of things I can create. This is a low-level, system programming language and you get to bare metal as close as you can. So I thought maybe implementing a specification of a file format can get my feet wet.
So I chose PNG. It naturally has a specification and thank-W3-gods it's free unlike many file specifications you can find on the internet. For instance, I have found PDF specification, which is reviewed by ISO and it costs 198 CHF. PNG was free.
Whatever, back to topic, this section of PNG specification says the first 8 bytes of PNG is as below:
137 80 78 71 13 10 26 10
And I thought testing this would be a good small practice. I have created a const
containing these first 8 bytes so that I can compare.
const VALID_PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
Then, we need a struct pointing to a std::io::File
.
struct PNG {
file: File
}
There are two built-in traits in Rust that will help me implement this PNG struct as a file-like thing. I have added those as comments and these will not be the topic of this post, I just show them so that you understand how implementing a file-like struct works.
// to read data from PNG file
// impl Read for PNG {}
// to deallocate PNG data on destruction
// impl Drop for PNG {}
// i might be wrong or lack things about this concept. if i do, you can ring my bell.
Later on, I have implemented raw PNG struct, see below:
impl PNG {
pub fn open(path: &str) -> Result<PNG, Error> {
let mut file = match File::open(path) {
Err(e) => return Err(e),
Ok(f) => f
};
if PNG::is_valid_signature(&mut file) {
Ok(PNG{file})
} else {
Err(Error::new(ErrorKind::Other, "File does not have a valid PNG signature."))
}
}
fn is_valid_signature(file: &mut File) -> bool {
let mut buffer = [0u8; 8];
let size = file.read(&mut buffer).unwrap();
if size < 8 {
false
} else {
buffer == VALID_PNG_SIGNATURE
}
}
}
Actually, both methods are self-explanatory but let's discuss it anyways. open
tries to open a std::io::File
and returns std::io::Error
if:
File
returnsError
or- The file is not a valid PNG.
is_valid_signature
does the validation. Basically, it returns false
if:
- The file size is less than 8 bytes or
- The first 8 bytes are not equal to
VALID_PNG_SIGNATURE
I can test it as below:
// assuming you have resources/64.png and resources/invalid.png in project root
// 64.png is generated with placeholder.com, is valid and 64x64
// invalid PNG is actually a text file containing the content "foo"
#[cfg(test)]
mod tests {
use crate::PNG;
#[test]
fn valid_png() {
let png_r = PNG::open("resources/64.png");
assert!(png_r.is_ok())
}
#[test]
fn invalid_png() {
let png_r = PNG::open("resources/invalid.png");
assert!(png_r.is_err())
}
}
Overall:
use std::fs::File;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Read;
const VALID_PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
struct PNG {
file: File,
}
impl PNG {
pub fn open(path: &str) -> Result<PNG, Error> {
let mut file = match File::open(path) {
Err(e) => return Err(e),
Ok(f) => f,
};
if PNG::is_valid_signature(&mut file) {
Ok(PNG { file })
} else {
Err(Error::new(
ErrorKind::Other,
"File does not have a valid PNG signature.",
))
}
}
fn is_valid_signature(file: &mut File) -> bool {
let mut buffer = [0u8; 8];
let size = file.read(&mut buffer).unwrap();
if size < 8 {
false
} else {
buffer == VALID_PNG_SIGNATURE
}
}
}
// impl Read for PNG {}
// impl Drop for PNG {}
#[cfg(test)]
mod tests {
use crate::PNG;
#[test]
fn valid_png() {
let png_r = PNG::open("resources/64.png");
assert!(png_r.is_ok())
}
#[test]
fn invalid_png() {
let png_r = PNG::open("resources/invalid.png");
assert!(png_r.is_err())
}
}