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 returns Error 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())
    }
}