xcursor/
parser.rs

1use std::{
2    fmt::{self, Debug, Formatter},
3    io::{Cursor, Error, ErrorKind, Read, Result as IoResult, Seek, SeekFrom},
4};
5
6#[derive(Debug, Clone, Eq, PartialEq)]
7struct Toc {
8    toctype: u32,
9    subtype: u32,
10    pos: u32,
11}
12
13/// A struct representing an image.
14/// Pixels are in ARGB format, with each byte representing a single channel.
15#[derive(Clone, Eq, PartialEq, Debug)]
16pub struct Image {
17    /// The nominal size of the image.
18    pub size: u32,
19
20    /// The actual width of the image. Doesn't need to match `size`.
21    pub width: u32,
22
23    /// The actual height of the image. Doesn't need to match `size`.
24    pub height: u32,
25
26    /// The X coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated)
27    pub xhot: u32,
28
29    /// The Y coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated)
30    pub yhot: u32,
31
32    /// The amount of time (in milliseconds) that this image should be shown for, before switching to the next.
33    pub delay: u32,
34
35    /// A slice containing the pixels' bytes, in RGBA format (or, in the order of the file).
36    pub pixels_rgba: Vec<u8>,
37
38    /// A slice containing the pixels' bytes, in ARGB format.
39    pub pixels_argb: Vec<u8>,
40}
41
42impl std::fmt::Display for Image {
43    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44        f.debug_struct("Image")
45            .field("size", &self.size)
46            .field("width", &self.width)
47            .field("height", &self.height)
48            .field("xhot", &self.xhot)
49            .field("yhot", &self.yhot)
50            .field("delay", &self.delay)
51            .field("pixels", &"/* omitted */")
52            .finish()
53    }
54}
55
56fn parse_header(i: &mut impl Read) -> IoResult<(u32, u32)> {
57    i.tag(*b"Xcur")?;
58    let header = i.u32_le()?;
59    let _version = i.u32_le()?;
60    let ntoc = i.u32_le()?;
61
62    Ok((header, ntoc))
63}
64
65fn parse_toc(i: &mut impl Read) -> IoResult<Toc> {
66    let toctype = i.u32_le()?; // Type
67    let subtype = i.u32_le()?; // Subtype
68    let pos = i.u32_le()?; // Position
69
70    Ok(Toc {
71        toctype,
72        subtype,
73        pos,
74    })
75}
76
77fn parse_img(i: &mut impl Read) -> IoResult<Image> {
78    i.tag([0x24, 0x00, 0x00, 0x00])?; // Header size
79    i.tag([0x02, 0x00, 0xfd, 0xff])?; // Type
80    let size = i.u32_le()?;
81    i.tag([0x01, 0x00, 0x00, 0x00])?; // Image version (1)
82    let width = i.u32_le()?;
83    let height = i.u32_le()?;
84    let xhot = i.u32_le()?;
85    let yhot = i.u32_le()?;
86    let delay = i.u32_le()?;
87
88    // Check image is well-formed. Taken from https://gitlab.freedesktop.org/xorg/lib/libxcursor/-/blob/09617bcc9a0f1b5072212da5f8fede92ab85d157/src/file.c#L456-463
89    if width > 0x7fff || height > 0x7fff {
90        return Err(Error::new(ErrorKind::Other, "Image too large"));
91    }
92    if width == 0 || height == 0 {
93        return Err(Error::new(
94            ErrorKind::Other,
95            "Image with zero width or height",
96        ));
97    }
98    if xhot > width || yhot > height {
99        return Err(Error::new(ErrorKind::Other, "Hotspot outside image"));
100    }
101
102    let img_length: usize = (4 * width * height) as usize;
103    let pixels_rgba = i.take_bytes(img_length)?;
104    let pixels_argb = rgba_to_argb(&pixels_rgba);
105
106    Ok(Image {
107        size,
108        width,
109        height,
110        xhot,
111        yhot,
112        delay,
113        pixels_argb,
114        pixels_rgba,
115    })
116}
117
118/// Converts a RGBA slice into an ARGB vec
119///
120/// Note that, if the input length is not
121/// a multiple of 4, the extra elements are ignored.
122fn rgba_to_argb(i: &[u8]) -> Vec<u8> {
123    let mut res = Vec::with_capacity(i.len());
124
125    for rgba in i.chunks_exact(4) {
126        res.push(rgba[3]);
127        res.push(rgba[0]);
128        res.push(rgba[1]);
129        res.push(rgba[2]);
130    }
131
132    res
133}
134
135/// Parse an XCursor file into its images.
136pub fn parse_xcursor(content: &[u8]) -> Option<Vec<Image>> {
137    parse_xcursor_stream(&mut Cursor::new(content)).ok()
138}
139
140/// Parse an XCursor file into its images.
141pub fn parse_xcursor_stream<R: Read + Seek>(input: &mut R) -> IoResult<Vec<Image>> {
142    let (header, ntoc) = parse_header(input)?;
143    input.seek(SeekFrom::Start(header as u64))?;
144
145    let mut img_indices = Vec::new();
146    for _ in 0..ntoc {
147        let toc = parse_toc(input)?;
148
149        if toc.toctype == 0xfffd_0002 {
150            img_indices.push(toc.pos);
151        }
152    }
153
154    let mut imgs = Vec::with_capacity(ntoc as usize);
155    for index in img_indices {
156        input.seek(SeekFrom::Start(index.into()))?;
157        imgs.push(parse_img(input)?);
158    }
159
160    Ok(imgs)
161}
162
163trait StreamExt {
164    /// Parse a series of bytes, returning `None` if it doesn't exist.
165    fn tag(&mut self, tag: [u8; 4]) -> IoResult<()>;
166
167    /// Take a slice of bytes.
168    fn take_bytes(&mut self, len: usize) -> IoResult<Vec<u8>>;
169
170    /// Parse a 32-bit little endian number.
171    fn u32_le(&mut self) -> IoResult<u32>;
172}
173
174impl<R: Read> StreamExt for R {
175    fn tag(&mut self, tag: [u8; 4]) -> IoResult<()> {
176        let mut data = [0u8; 4];
177        self.read_exact(&mut data)?;
178        if data != tag {
179            Err(Error::new(ErrorKind::Other, "Tag mismatch"))
180        } else {
181            Ok(())
182        }
183    }
184
185    fn take_bytes(&mut self, len: usize) -> IoResult<Vec<u8>> {
186        let mut data = vec![0; len];
187        self.read_exact(&mut data)?;
188        Ok(data)
189    }
190
191    fn u32_le(&mut self) -> IoResult<u32> {
192        let mut data = [0u8; 4];
193        self.read_exact(&mut data)?;
194        Ok(u32::from_le_bytes(data))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::{parse_header, parse_toc, parse_xcursor, rgba_to_argb, Image, Toc};
201    use std::io::Cursor;
202
203    // A sample (and simple) XCursor file generated with xcursorgen.
204    // Contains a single 4x4 image.
205    const FILE_CONTENTS: [u8; 128] = [
206        0x58, 0x63, 0x75, 0x72, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
207        0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00,
208        0x00, 0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04,
209        0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
210        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
211        0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
212        0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00,
213        0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
214        0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
215    ];
216
217    #[test]
218    fn test_parse_header() {
219        let mut cursor = Cursor::new(&FILE_CONTENTS[..]);
220        assert_eq!(parse_header(&mut cursor).unwrap(), (16, 1));
221        assert_eq!(cursor.position(), 16);
222    }
223
224    #[test]
225    fn test_parse_toc() {
226        let toc = Toc {
227            toctype: 0xfffd0002,
228            subtype: 4,
229            pos: 0x1c,
230        };
231        let mut cursor = Cursor::new(&FILE_CONTENTS[16..]);
232        assert_eq!(parse_toc(&mut cursor).unwrap(), toc);
233        assert_eq!(cursor.position(), 28 - 16);
234    }
235
236    #[test]
237    fn test_parse_image() {
238        // The image always repeats the same pixels across its 4 x 4 pixels
239        let make_pixels = |pixel: [u8; 4]| {
240            // This is just "pixels.repeat(4 * 4)", but working in Rust 1.34
241            std::iter::repeat(pixel)
242                .take(4 * 4)
243                .flat_map(|p| p.iter().cloned().collect::<Vec<_>>())
244                .collect()
245        };
246        let expected = Image {
247            size: 4,
248            width: 4,
249            height: 4,
250            xhot: 1,
251            yhot: 1,
252            delay: 1,
253            pixels_rgba: make_pixels([0, 0, 0, 128]),
254            pixels_argb: make_pixels([128, 0, 0, 0]),
255        };
256        assert_eq!(Some(vec![expected]), parse_xcursor(&FILE_CONTENTS));
257    }
258
259    #[test]
260    fn test_one_image_three_times() {
261        let data = [
262            b'X', b'c', b'u', b'r', // magic
263            0x10, 0x00, 0x00, 0x00, // header file offset (16)
264            0x00, 0x00, 0x00, 0x00, // version
265            0x03, 0x00, 0x00, 0x00, // num TOC entries, 3
266            // TOC
267            0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
268            0x04, 0x00, 0x00, 0x00, // size 4
269            0x34, 0x00, 0x00, 0x00, // image offset (52)
270            0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
271            0x03, 0x00, 0x00, 0x00, // size 3
272            0x34, 0x00, 0x00, 0x00, // image offset (52)
273            0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
274            0x04, 0x00, 0x00, 0x00, // size 4
275            0x34, 0x00, 0x00, 0x00, // image offset (52)
276            // image
277            0x24, 0x00, 0x00, 0x00, // header
278            0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
279            0x04, 0x00, 0x00, 0x00, // size 4
280            0x01, 0x00, 0x00, 0x00, // version
281            0x01, 0x00, 0x00, 0x00, // width 1
282            0x01, 0x00, 0x00, 0x00, // height 1
283            0x00, 0x00, 0x00, 0x00, // x_hot 0
284            0x00, 0x00, 0x00, 0x00, // y_hot 0
285            0x00, 0x00, 0x00, 0x00, // delay 0
286            0x12, 0x34, 0x56, 0x78, // pixel
287        ];
288        let expected = Image {
289            size: 4,
290            width: 1,
291            height: 1,
292            xhot: 0,
293            yhot: 0,
294            delay: 0,
295            pixels_rgba: vec![0x12, 0x34, 0x56, 0x78],
296            pixels_argb: vec![0x78, 0x12, 0x34, 0x56],
297        };
298        assert_eq!(
299            Some(vec![expected.clone(), expected.clone(), expected.clone()]),
300            parse_xcursor(&data)
301        );
302    }
303
304    #[test]
305    fn test_rgba_to_argb() {
306        let initial: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
307
308        assert_eq!(rgba_to_argb(&initial), [3u8, 0, 1, 2, 7, 4, 5, 6])
309    }
310
311    #[test]
312    fn test_rgba_to_argb_extra_items() {
313        let initial: [u8; 9] = [0, 1, 2, 3, 4, 5, 6, 7, 8];
314
315        assert_eq!(rgba_to_argb(&initial), &[3u8, 0, 1, 2, 7, 4, 5, 6]);
316    }
317
318    #[test]
319    fn test_rgba_to_argb_no_items() {
320        let initial: &[u8] = &[];
321
322        assert_eq!(initial, &rgba_to_argb(initial)[..]);
323    }
324}