Chapter 1: Chunk Types
Okay here we go!
The next three chapters will have you implement a basic PNG file. PNG files are essentially just a list of "chunks", each containing their own data. Each chunk has a type that can be represented as a 4 character string. There are standard chunk types for things like image data, but there's no rule that would prevent you from inserting your own chunks with whatever data you want. We can even tell PNG decoders to ignore our chunks depending on how we capitalize our chunk types.
In this chapter, we'll be implementing chunk types. These are pretty easy since they're essentially just 4 alphabetic characters. Your chunk types should always be valid chunks. It should not be possible to construct an invalid chunk type using your public interface. The PNG file structure spec will tell what a valid chunk type looks like.
A little bit about bytes
I keep saying that chunk types are strings, but you'll want to think about them in terms of bytes. If you've never worked directly with bytes before, it's not that scary. If you have worked directly with bytes before, you can skip straight to the Assignment section below.
A bit is a 0
or a 1
. A byte is 8 bits. In decimal notation, a byte is a number in the range 0 to 255. You've probably heard that computers only understand 1's and 0's. While that's kinda true, they generally communicate using bytes. In Rust, bytes are represented by the type u8
.
Bytes are fun because their meaning is usually open to interpretation. We've already seen that individual bytes can be interpreted as 8 individual bits or as a number from 0 to 255. They can also be interpreted in groups. A 32-bit integer is 4 bytes. Rust even gives you a function to make a u32
from 4 bytes using u32::from_be_bytes([1, 2, 3, 4])
. A Rust String
is just a Vec<u8>
whose bytes have been validated as UTF-8. Guess what the "8" in UTF-8 stands for.
Now let's make some chunks.
Assignment
Using the PNG file structure spec, implement chunk types. Don't worry about implementing any specific chunk types. You only need to store a valid PNG chunk type. You don't have to implement the full chunk object either. We'll do that in the next chapter. This is just the 4 byte chunk type described in section 3.3
of the link above.
You need to provide methods that return the chunk type in bytes, check the validity of the entire chunk type, and check the special meaning of capitalization for each of the four bytes. Method signatures are provided below.
You will also need to implement a few standard library traits.
Requirements
- Copy the unit tests below and paste them at the bottom of your
chunk_type.rs
file. - Write a
ChunkType
struct with your implementation of PNG chunk types. - Implement
TryFrom<[u8; 4]>
for yourChunkType
. - Implement
FromStr
for yourChunkType
. - Implement
Display
for yourChunkType
. - Implement or derive
PartialEq
andEq
for yourChunkType
- Required methods:
fn bytes(&self) -> [u8; 4]
fn is_valid(&self) -> bool
fn is_critical(&self) -> bool
fn is_public(&self) -> bool
fn is_reserved_bit_valid(&self) -> bool
fn is_safe_to_copy(&self) -> bool
- Pass all of the unit tests.
Unit Tests
#![allow(unused_variables)] fn main() { #[cfg(test)] mod tests { use super::*; use std::convert::TryFrom; use std::str::FromStr; #[test] pub fn test_chunk_type_from_bytes() { let expected = [82, 117, 83, 116]; let actual = ChunkType::try_from([82, 117, 83, 116]).unwrap(); assert_eq!(expected, actual.bytes()); } #[test] pub fn test_chunk_type_from_str() { let expected = ChunkType::try_from([82, 117, 83, 116]).unwrap(); let actual = ChunkType::from_str("RuSt").unwrap(); assert_eq!(expected, actual); } #[test] pub fn test_chunk_type_is_critical() { let chunk = ChunkType::from_str("RuSt").unwrap(); assert!(chunk.is_critical()); } #[test] pub fn test_chunk_type_is_not_critical() { let chunk = ChunkType::from_str("ruSt").unwrap(); assert!(!chunk.is_critical()); } #[test] pub fn test_chunk_type_is_public() { let chunk = ChunkType::from_str("RUSt").unwrap(); assert!(chunk.is_public()); } #[test] pub fn test_chunk_type_is_not_public() { let chunk = ChunkType::from_str("RuSt").unwrap(); assert!(!chunk.is_public()); } #[test] pub fn test_chunk_type_is_reserved_bit_valid() { let chunk = ChunkType::from_str("RuSt").unwrap(); assert!(chunk.is_reserved_bit_valid()); } #[test] pub fn test_chunk_type_is_reserved_bit_invalid() { let chunk = ChunkType::from_str("Rust").unwrap(); assert!(!chunk.is_reserved_bit_valid()); } #[test] pub fn test_chunk_type_is_safe_to_copy() { let chunk = ChunkType::from_str("RuSt").unwrap(); assert!(chunk.is_safe_to_copy()); } #[test] pub fn test_chunk_type_is_unsafe_to_copy() { let chunk = ChunkType::from_str("RuST").unwrap(); assert!(!chunk.is_safe_to_copy()); } #[test] pub fn test_valid_chunk_is_valid() { let chunk = ChunkType::from_str("RuSt").unwrap(); assert!(chunk.is_valid()); } #[test] pub fn test_invalid_chunk_is_valid() { let chunk = ChunkType::from_str("Rust").unwrap(); assert!(!chunk.is_valid()); let chunk = ChunkType::from_str("Ru1t"); assert!(chunk.is_err()); } #[test] pub fn test_chunk_type_string() { let chunk = ChunkType::from_str("RuSt").unwrap(); assert_eq!(&chunk.to_string(), "RuSt"); } #[test] pub fn test_chunk_type_trait_impls() { let chunk_type_1: ChunkType = TryFrom::try_from([82, 117, 83, 116]).unwrap(); let chunk_type_2: ChunkType = FromStr::from_str("RuSt").unwrap(); let _chunk_string = format!("{}", chunk_type_1); let _are_chunks_equal = chunk_type_1 == chunk_type_2; } } }