Skip to main content

tiff_reader/
header.rs

1use crate::error::{Error, Result};
2
3pub use tiff_core::ByteOrder;
4
5/// Parsed TIFF/BigTIFF file header.
6#[derive(Debug, Clone)]
7pub struct TiffHeader {
8    pub byte_order: ByteOrder,
9    /// 42 for classic TIFF, 43 for BigTIFF.
10    pub version: u16,
11    /// Offset to the first IFD.
12    pub first_ifd_offset: u64,
13}
14
15impl TiffHeader {
16    /// Returns `true` if this is a BigTIFF file (version 43).
17    pub fn is_bigtiff(&self) -> bool {
18        self.version == 43
19    }
20
21    /// Parse the TIFF header from raw bytes.
22    pub fn parse(data: &[u8]) -> Result<Self> {
23        if data.len() < 8 {
24            return Err(Error::InvalidMagic);
25        }
26
27        let byte_order = match &data[0..2] {
28            b"II" => ByteOrder::LittleEndian,
29            b"MM" => ByteOrder::BigEndian,
30            _ => return Err(Error::InvalidMagic),
31        };
32
33        let read_u16 = |offset: usize| -> u16 {
34            let bytes = [data[offset], data[offset + 1]];
35            match byte_order {
36                ByteOrder::LittleEndian => u16::from_le_bytes(bytes),
37                ByteOrder::BigEndian => u16::from_be_bytes(bytes),
38            }
39        };
40
41        let version = read_u16(2);
42
43        match version {
44            42 => {
45                // Classic TIFF: 4-byte IFD offset at position 4
46                let read_u32 = |offset: usize| -> u32 {
47                    let bytes: [u8; 4] = data[offset..offset + 4].try_into().unwrap();
48                    match byte_order {
49                        ByteOrder::LittleEndian => u32::from_le_bytes(bytes),
50                        ByteOrder::BigEndian => u32::from_be_bytes(bytes),
51                    }
52                };
53                let first_ifd_offset = read_u32(4) as u64;
54                Ok(Self {
55                    byte_order,
56                    version,
57                    first_ifd_offset,
58                })
59            }
60            43 => {
61                // BigTIFF: 2-byte offset size (must be 8), 2-byte reserved, 8-byte IFD offset
62                if data.len() < 16 {
63                    return Err(Error::InvalidMagic);
64                }
65                let offset_size = read_u16(4);
66                if offset_size != 8 {
67                    return Err(Error::InvalidBigTiffHeader(format!(
68                        "offset size must be 8, found {offset_size}"
69                    )));
70                }
71                let reserved = read_u16(6);
72                if reserved != 0 {
73                    return Err(Error::InvalidBigTiffHeader(format!(
74                        "reserved field must be zero, found {reserved}"
75                    )));
76                }
77                let read_u64 = |offset: usize| -> u64 {
78                    let bytes: [u8; 8] = data[offset..offset + 8].try_into().unwrap();
79                    match byte_order {
80                        ByteOrder::LittleEndian => u64::from_le_bytes(bytes),
81                        ByteOrder::BigEndian => u64::from_be_bytes(bytes),
82                    }
83                };
84                let first_ifd_offset = read_u64(8);
85                Ok(Self {
86                    byte_order,
87                    version,
88                    first_ifd_offset,
89                })
90            }
91            _ => Err(Error::UnsupportedVersion(version)),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn parse_little_endian_classic() {
102        // II, version 42, IFD offset at 8
103        let data = b"II\x2a\x00\x08\x00\x00\x00";
104        let header = TiffHeader::parse(data).unwrap();
105        assert_eq!(header.byte_order, ByteOrder::LittleEndian);
106        assert_eq!(header.version, 42);
107        assert_eq!(header.first_ifd_offset, 8);
108        assert!(!header.is_bigtiff());
109    }
110
111    #[test]
112    fn parse_big_endian_classic() {
113        // MM, version 42, IFD offset at 8
114        let data = b"MM\x00\x2a\x00\x00\x00\x08";
115        let header = TiffHeader::parse(data).unwrap();
116        assert_eq!(header.byte_order, ByteOrder::BigEndian);
117        assert_eq!(header.version, 42);
118        assert_eq!(header.first_ifd_offset, 8);
119    }
120
121    #[test]
122    fn parse_bigtiff() {
123        // II, version 43, offset size 8, reserved 0, IFD offset at 16
124        let mut data = Vec::new();
125        data.extend_from_slice(b"II"); // byte order
126        data.extend_from_slice(&43u16.to_le_bytes()); // version
127        data.extend_from_slice(&8u16.to_le_bytes()); // offset size
128        data.extend_from_slice(&0u16.to_le_bytes()); // reserved
129        data.extend_from_slice(&16u64.to_le_bytes()); // first IFD offset
130        let header = TiffHeader::parse(&data).unwrap();
131        assert!(header.is_bigtiff());
132        assert_eq!(header.first_ifd_offset, 16);
133    }
134
135    #[test]
136    fn reject_invalid_magic() {
137        let data = b"XX\x2a\x00\x08\x00\x00\x00";
138        assert!(TiffHeader::parse(data).is_err());
139    }
140
141    #[test]
142    fn rejects_invalid_bigtiff_offset_size_and_reserved_field() {
143        let mut data = Vec::new();
144        data.extend_from_slice(b"II");
145        data.extend_from_slice(&43u16.to_le_bytes());
146        data.extend_from_slice(&4u16.to_le_bytes());
147        data.extend_from_slice(&0u16.to_le_bytes());
148        data.extend_from_slice(&16u64.to_le_bytes());
149        assert!(matches!(
150            TiffHeader::parse(&data),
151            Err(Error::InvalidBigTiffHeader(message)) if message.contains("offset size")
152        ));
153
154        data[4..6].copy_from_slice(&8u16.to_le_bytes());
155        data[6..8].copy_from_slice(&1u16.to_le_bytes());
156        assert!(matches!(
157            TiffHeader::parse(&data),
158            Err(Error::InvalidBigTiffHeader(message)) if message.contains("reserved")
159        ));
160    }
161}