image_extras/xpm/
mod.rs

1//! Decoding of XPM Images
2//!
3//! XPM (X PixMap) Format is a plain text image format, originally designed to store
4//! cursor and icon data. XPM images are valid C code.
5//!
6//! (This format is obsolete and nobody should make new images in it. If you need to
7//! include an image in a C program, use `xxd -i` or #embed.)
8//!
9//! The XPM format allows for encoding an image which can be expressed differently
10//! depending on the display capabilities (X11 visual), providing specialized versions
11//! for color, grayscale, black and white, etc. output in the same image. In practice,
12//! most XPM images created after the mid 1990s only provide a variant for the color
13//! visual. As a result, this decoder implementation only outputs the color version
14//! of the input image.
15//!
16//! A number of features of the original libXpm are not supported (because they appear to very
17//! rarely have been used):
18//! - XPMEXT extensions
19//! - HSV color specifications
20//! - Output for non-color visuals
21//! - More relaxed header comment parsing (allowing different whitespace around `XPM` in `/* XPM */`)
22//! - Loading with a different color table
23//!
24//! This is a somewhat strict decoder and will reject many broken image files, including:
25//! - those using the XPM2 header or `static char ** name = {` array string
26//! - those missing a trailing "," on lines, or which use ";" instead of ","
27//! - those with color data lines that are too long
28//! - those which have content after the final semicolon which is not a C comment
29//!
30//! Note: color values for the X11 color name table were _changed_ for the X11R4 release
31//! in Dec 1989; since then there have only been additions.
32//!
33//! This overlaps with XPM version development: XPMv1 in Feb 1989, XPMv2 in Feb-August 1990,
34//! and XPMv3 in April 1991. Therefore, if you _do_ see an ancient XPMv1 or XPMv2 file
35//! somewhere, it may be using different color name values.
36//!
37//! This decoder uses the X11 color name table as of X11R6 (May 1994); the only additions since
38//! then, in 2014 to add some CSS color names, are _not_ included, to preserve compatibility
39//! with other XPM parsers.
40//!
41//! # Related Links
42//! * <https://www.x.org/docs/XPM/xpm.pdf> - XPM Manual version 3.4i, which specifies the format
43//! * <https://web.archive.org/web/20060702022929/http://koala.ilog.fr/ftp/pub/xpm/xpm-3-paper.ps.gz> - XPM Paper
44//! * <https://en.wikipedia.org/wiki/X_PixMap> - The XPM format on wikipedia
45//! * <https://web.archive.org/web/20110513234507/https://www.w3.org/People/danield/xpm_story.html> - XPM format history
46//! * <https://gitlab.freedesktop.org/xorg/app/rgb/raw/master/rgb.txt> - X color names
47//! * <https://www.x.org/wiki/X11R4/#index10h4> - Introduction of modern X11 color name table
48//! * <https://web.archive.org/web/20070808230118/http://koala.ilog.fr/ftp/pub/xpm/> - more historical XPM material
49
50mod x11r6colors;
51
52use std::cmp::Ordering;
53use std::fmt;
54use std::io::{BufRead, Bytes};
55
56use image::error::{
57    DecodingError, ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind,
58};
59use image::{ColorType, ImageDecoder, LimitSupport, Limits};
60
61/// Maximum length of an X11/CSS/etc. color name is 20; and of an RGB color is 13
62const MAX_COLOR_NAME_LEN: usize = 32;
63
64/// Location of a byte in the input stream.
65///
66/// Includes byte offset (for format debugging with hex editor) and
67/// line:column offset (for format debugging with text editor)
68#[derive(Clone, Copy, Debug)]
69struct TextLocation {
70    byte: u64,
71    line: u64,
72    column: u64,
73}
74
75/// A peekable reader which tracks location information
76struct TextReader<R> {
77    inner: R,
78
79    current: Option<u8>,
80
81    location: TextLocation,
82}
83
84impl<R> TextReader<R>
85where
86    R: Iterator<Item = u8>,
87{
88    /// Initialize a TextReader
89    fn new(mut r: R) -> TextReader<R> {
90        let current = r.next();
91        TextReader {
92            inner: r,
93            current,
94            location: TextLocation {
95                byte: 0,
96                line: 1,
97                column: 0,
98            },
99        }
100    }
101
102    /// Consume the next byte. On EOF, will return None
103    fn next(&mut self) -> Option<u8> {
104        self.current?;
105
106        let mut current = self.inner.next();
107        std::mem::swap(&mut self.current, &mut current);
108
109        self.location.byte += 1;
110        self.location.column += 1;
111        if let Some(b'\n') = current {
112            self.location.line += 1;
113            self.location.column = 0;
114        }
115        current
116    }
117    /// Peek at the next byte. On EOF, will return None
118    fn peek(&self) -> Option<u8> {
119        self.current
120    }
121    /// The location of the last byte returned by [Self::next]
122    fn loc(&self) -> TextLocation {
123        self.location
124    }
125}
126
127/// Helper struct to project BufRead down to Iterator<Item=u8>. Costs of this simple
128/// lifetime-free abstraction include that the struct requires space to store the
129/// error value, and that code using this must eventually check the error field.
130struct IoAdapter<R> {
131    reader: Bytes<R>,
132    error: Option<std::io::Error>,
133}
134
135impl<R> Iterator for IoAdapter<R>
136where
137    R: BufRead,
138{
139    type Item = u8;
140    #[inline(always)]
141    fn next(&mut self) -> Option<Self::Item> {
142        if self.error.is_some() {
143            return None;
144        }
145        match self.reader.next() {
146            None => None,
147            Some(Ok(v)) => Some(v),
148            Some(Err(e)) => {
149                self.error = Some(e);
150                None
151            }
152        }
153    }
154}
155
156/// XPM decoder
157pub struct XpmDecoder<R> {
158    r: TextReader<IoAdapter<R>>,
159    info: XpmHeaderInfo,
160}
161
162/// Key XPM file properties determined from first line
163struct XpmHeaderInfo {
164    width: u32,
165    height: u32,
166    ncolors: u32,
167    /// characters per pixel
168    cpp: u32,
169}
170
171/// XPM color palette storage
172struct XpmPalette {
173    /// Sorted table of color code entries. There are many possible ways to store
174    /// this, and the fastest approach depends on the image structure, number of pixels,
175    /// and number of colors. While not as efficient to construct as an unsorted list,
176    /// or as efficient to look values up in as a perfect hash table, the sorted table
177    /// performs decently well as long as the palette is small enough to fit in CPU caches.
178    table: Vec<XpmColorCodeEntry>,
179}
180
181/// Pixel code and value read from the Colors section of an XPM file
182struct XpmColorCodeEntry {
183    code: u64,
184    /// channel order: R,G,B,A
185    value: [u16; 4],
186}
187
188#[derive(Debug, Clone, Copy)]
189enum XpmPart {
190    Header,
191    ArrayStart,
192    FirstLine,
193    Palette,
194    Body,
195    Trailing,
196    AfterEnd,
197}
198
199#[derive(Debug)]
200enum XpmDecodeError {
201    Parse(XpmPart, TextLocation),
202    ZeroWidth,
203    ZeroHeight,
204    ZeroColors,
205    BadCharsPerColor(u32),
206    // A color with the given name is not available.
207    // Name provided in buffer, length format, and should be alphanumeric ASCII
208    UnknownColor(([u8; MAX_COLOR_NAME_LEN], u8)),
209    // Palette entry is missing 'c'-type color specification
210    NoColorModeColorSpecified,
211    BadHexColor,
212    DuplicateCode,
213    UnknownCode,
214    TwoKeysInARow,
215    MissingEntry,
216    MissingColorAfterKey,
217    MissingKeyBeforeColor,
218    InvalidColorName,
219    ColorNameTooLong,
220}
221
222/// Types of visuals for which a color should be used
223#[derive(Debug)]
224enum XpmVisual {
225    Mono,
226    Symbolic,
227    Grayscale4,
228    Grayscale,
229    Color,
230}
231
232impl fmt::Display for TextLocation {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        f.write_fmt(format_args!(
235            "byte={},line={}:col={}",
236            self.byte, self.line, self.column
237        ))
238    }
239}
240
241impl fmt::Display for XpmPart {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        match self {
244            Self::Header => f.write_str("header"),
245            Self::ArrayStart => f.write_str("array definition"),
246            Self::FirstLine => f.write_str("<Values> section"),
247            Self::Palette => f.write_str("<Colors> section"),
248            Self::Body => f.write_str("<Pixels> section"),
249            Self::Trailing => f.write_str("array end"),
250            Self::AfterEnd => f.write_str("after final semicolon"),
251        }
252    }
253}
254
255impl fmt::Display for XpmDecodeError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Self::Parse(part, loc) => f.write_fmt(format_args!("Failed to parse {}, at {}", part, loc)),
259            Self::ZeroWidth => f.write_str("Invalid (zero) image width"),
260            Self::ZeroHeight => f.write_str("Invalid (zero) image height"),
261            Self::ZeroColors => f.write_str("Invalid (zero) number of colors"),
262            Self::BadCharsPerColor(c) => f.write_fmt(format_args!(
263                "Invalid number of characters per color: {} is not in [1,8]",
264                c
265            )),
266            Self::UnknownColor((buf, len)) => {
267                let s = std::str::from_utf8(&buf[..*len as usize]).ok().unwrap_or("");
268                assert!(s.chars().all(|x| x.is_ascii_alphanumeric()));
269                f.write_fmt(format_args!("Unknown color name \"{}\"; is not an X11R6 color.", s))
270            }
271            Self::NoColorModeColorSpecified => {
272                f.write_str("Color entry has no specified value for color visual")
273            }
274            Self::BadHexColor => f.write_str("Invalid hex RGB color"),
275            Self::DuplicateCode => f.write_str("Duplicate color code"),
276            Self::UnknownCode => f.write_str("Unknown color code"),
277
278            Self::ColorNameTooLong => f.write_str("Invalid color name, too long"),
279            Self::TwoKeysInARow => f.write_str("Invalid color specification, two keys in a row"),
280            Self::MissingEntry => f.write_str("Invalid color specification, must contain at least one key-color pair"),
281            Self::MissingColorAfterKey => f.write_str("Invalid color specification, no color name after key"),
282            Self::MissingKeyBeforeColor => f.write_str("Invalid color specification, no key before color name or could not parse value as key (m|s|g4|g|c)"),
283            Self::InvalidColorName => f.write_str("Invalid color name, contains non-alphanumeric or non-whitespace characters"),
284        }
285    }
286}
287
288impl std::error::Error for XpmDecodeError {}
289
290impl From<XpmDecodeError> for ImageError {
291    fn from(e: XpmDecodeError) -> ImageError {
292        ImageError::Decoding(DecodingError::new(ImageFormatHint::Name("XPM".into()), e))
293    }
294}
295
296/// Helper trait for the pattern in which, after calling a function returning a Result,
297/// one wishes to use an error from a different source.
298trait XpmDecoderIoInjectionExt {
299    type Value;
300    fn apply_after(self, err: &mut Option<std::io::Error>) -> Result<Self::Value, ImageError>;
301}
302
303impl<X> XpmDecoderIoInjectionExt for Result<X, XpmDecodeError> {
304    type Value = X;
305    fn apply_after(self, err: &mut Option<std::io::Error>) -> Result<Self::Value, ImageError> {
306        if let Some(err) = err.take() {
307            return Err(ImageError::IoError(err));
308        }
309        match self {
310            Self::Ok(x) => Ok(x),
311            Self::Err(e) => Err(e.into()),
312        }
313    }
314}
315
316/// Is x a valid character to use in a word of a color name
317fn valid_name_char(x: u8) -> bool {
318    // underscore: used in some symbolic names
319    matches!(x, b'#' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_')
320}
321/// Replace upper case by lower case ASCII letters
322fn fold_to_lower(x: u8) -> u8 {
323    match x {
324        b'A'..=b'Z' => (x - b'A') + b'a',
325        _ => x,
326    }
327}
328
329/// Read precisely the string `s` from `r`, or error.
330fn read_fixed_string<R: Iterator<Item = u8>>(
331    r: &mut TextReader<R>,
332    s: &[u8],
333    part: XpmPart,
334) -> Result<(), XpmDecodeError> {
335    for c in s {
336        if let Some(b) = r.next() {
337            if b != *c {
338                return Err(XpmDecodeError::Parse(part, r.loc()));
339            }
340        } else {
341            return Err(XpmDecodeError::Parse(part, r.loc()));
342        };
343    }
344    Ok(())
345}
346// Read a single byte
347fn read_byte<R: Iterator<Item = u8>>(
348    r: &mut TextReader<R>,
349    part: XpmPart,
350) -> Result<u8, XpmDecodeError> {
351    match r.next() {
352        None => Err(XpmDecodeError::Parse(part, r.loc())),
353        Some(b) => Ok(b),
354    }
355}
356
357/// Read a mixture of ' ' and '\t'. At least one character must be read.
358// Other whitespace characters are not permitted.
359fn read_whitespace_gap<R: Iterator<Item = u8>>(
360    r: &mut TextReader<R>,
361    part: XpmPart,
362) -> Result<(), XpmDecodeError> {
363    let b = read_byte(r, part)?;
364    if !(b == b' ' || b == b'\t') {
365        return Err(XpmDecodeError::Parse(part, r.loc()));
366    }
367    while let Some(b) = r.peek() {
368        if b == b' ' || b == b'\t' {
369            r.next();
370            continue;
371        } else {
372            return Ok(());
373        }
374    }
375    Ok(())
376}
377
378/// Read a mixture of ' ', '\t', '\n', and C-style /* comments */.
379/// This will error if it sees a / without following *
380fn skip_whitespace_and_comments<R: Iterator<Item = u8>>(
381    r: &mut TextReader<R>,
382    part: XpmPart,
383) -> Result<usize, XpmDecodeError> {
384    let mut nbytes = 0;
385
386    // `has_first_char`: If out of comment, has / ; if in comment, has *
387    let mut has_first_char = false;
388    let mut in_comment = false;
389
390    while let Some(b) = r.peek() {
391        if !in_comment {
392            if has_first_char {
393                if b != b'*' {
394                    return Err(XpmDecodeError::Parse(part, r.loc()));
395                } else {
396                    in_comment = true;
397                    has_first_char = false;
398                }
399            }
400            if b == b'/' {
401                has_first_char = true;
402            }
403        }
404        if b == b' ' || b == b'\t' || b == b'\n' || b == b'/' || in_comment {
405            if in_comment {
406                if has_first_char && b == b'/' {
407                    in_comment = false;
408                }
409                has_first_char = b == b'*';
410            }
411            nbytes += 1;
412            r.next();
413            continue;
414        } else {
415            break;
416        }
417    }
418    if !in_comment && has_first_char {
419        // Parsed up to a / but did not find *
420        return Err(XpmDecodeError::Parse(part, r.loc()));
421    }
422
423    Ok(nbytes)
424}
425
426fn skip_spaces_and_tabs<R: Iterator<Item = u8>>(
427    r: &mut TextReader<R>,
428) -> Result<usize, XpmDecodeError> {
429    let mut nbytes = 0;
430    while let Some(b) = r.peek() {
431        if b == b' ' || b == b'\t' {
432            nbytes += 1;
433            r.next();
434            continue;
435        } else {
436            break;
437        }
438    }
439    Ok(nbytes)
440}
441
442/// Read a mixture of ' ' and '\t', until reading '\n'.
443fn read_to_newline<R: Iterator<Item = u8>>(
444    r: &mut TextReader<R>,
445    part: XpmPart,
446) -> Result<(), XpmDecodeError> {
447    while let Some(b) = r.peek() {
448        if b == b' ' || b == b'\t' {
449            r.next();
450            continue;
451        } else {
452            break;
453        }
454    }
455    if read_byte(r, part)? != b'\n' {
456        Err(XpmDecodeError::Parse(part, r.loc()))
457    } else {
458        Ok(())
459    }
460}
461/// Read token into the buffer until the buffer size is exceeded, or ' ' or '\t' or '"' is found
462/// \ characters are forbidden. Returns the region of data read.
463fn read_until_whitespace_or_eos<'a, R: Iterator<Item = u8>>(
464    r: &mut TextReader<R>,
465    buf: &'a mut [u8],
466    part: XpmPart,
467) -> Result<&'a mut [u8], XpmDecodeError> {
468    let mut len = 0;
469    while let Some(b) = r.peek() {
470        if b == b' ' || b == b'\t' || b == b'"' {
471            return Ok(&mut buf[..len]);
472        } else if b == b'\\' {
473            r.next();
474            return Err(XpmDecodeError::Parse(part, r.loc()));
475        } else {
476            if len >= buf.len() {
477                // identifier is too long
478                return Err(XpmDecodeError::Parse(part, r.loc()));
479            }
480            buf[len] = b;
481            len += 1;
482            r.next();
483        }
484    }
485    Ok(&mut buf[..len])
486}
487
488/// Read fixed length token into the buffer. Errors if file ends, or " or \ is found.
489fn read_all_except_eos<R: Iterator<Item = u8>>(
490    r: &mut TextReader<R>,
491    buf: &mut [u8],
492    part: XpmPart,
493) -> Result<(), XpmDecodeError> {
494    let mut len = 0;
495    while let Some(b) = r.peek() {
496        if b == b'"' || b == b'\\' {
497            r.next();
498            return Err(XpmDecodeError::Parse(part, r.loc()));
499        } else {
500            buf[len] = b;
501            len += 1;
502            r.next();
503            if len >= buf.len() {
504                return Ok(());
505            }
506        }
507    }
508    Err(XpmDecodeError::Parse(part, r.loc()))
509}
510
511/// Read the name portion of the file (but do not validate it, because some old files
512/// may put invalid characters here (like "." and "-") or use 8-bit character sets instead
513/// of Unicode.)
514fn read_name<R: Iterator<Item = u8>>(
515    r: &mut TextReader<R>,
516    part: XpmPart,
517) -> Result<(), XpmDecodeError> {
518    let mut empty = true;
519    while let Some(b) = r.peek() {
520        match b {
521            b'/' | b' ' | b'\t' | b'\n' | b'[' => {
522                break;
523            }
524            _ => (),
525        }
526        r.next();
527        empty = false;
528    }
529    if empty {
530        return Err(XpmDecodeError::Parse(part, r.loc()));
531    }
532
533    Ok(())
534}
535
536/// Parse string into integer, rejecting leading + and leading zeros
537fn parse_i32(data: &[u8]) -> Option<i32> {
538    if data.starts_with(b"-") {
539        (-(parse_u32(&data[1..])? as i64)).try_into().ok()
540    } else {
541        parse_u32(data)?.try_into().ok()
542    }
543}
544
545/// Parse string into unsigned integer, rejecting leading + and leading zeros
546fn parse_u32(data: &[u8]) -> Option<u32> {
547    let Some(c1) = data.first() else {
548        // Reject empty string
549        return None;
550    };
551    if *c1 == b'0' && data.len() > 1 {
552        // Reject leading zeros unless value is exactly zero
553        return None;
554    }
555    let mut x: u32 = 0;
556    for c in data {
557        if b'0' <= *c && *c <= b'9' {
558            x = x.checked_mul(10)?.checked_add((*c - b'0') as u32)?;
559        } else {
560            return None;
561        }
562    }
563    Some(x)
564}
565fn parse_hex(b: u8) -> Option<u8> {
566    match b {
567        b'0'..=b'9' => Some(b - b'0'),
568        b'A'..=b'F' => Some(b - b'A' + 10),
569        b'a'..=b'f' => Some(b - b'a' + 10),
570        _ => None,
571    }
572}
573fn parse_hex1(x1: u8) -> Option<u16> {
574    let x = parse_hex(x1)? as u16;
575    Some(x | (x << 4) | (x << 8) | (x << 12))
576}
577fn parse_hex2(x2: u8, x1: u8) -> Option<u16> {
578    let x = ((parse_hex(x2)? as u16) << 4) | (parse_hex(x1)? as u16);
579    Some(x | (x << 8))
580}
581fn parse_hex3(x3: u8, x2: u8, x1: u8) -> Option<u16> {
582    let x =
583        ((parse_hex(x3)? as u16) << 8) | ((parse_hex(x2)? as u16) << 4) | (parse_hex(x1)? as u16);
584    // There are four reasonable approaches to converting 12-bit to 16-bit,
585    // round down, round nearest, round up, and round fast
586    // (x*65535)/4095, (x*65535+2047)/4095, (x*65535+4094)/4095, and (x<<4)|(x>>8).
587    Some((((x as u32) * 65535 + 2047) / 4095) as u16)
588}
589fn parse_hex4(x4: u8, x3: u8, x2: u8, x1: u8) -> Option<u16> {
590    Some(
591        (parse_hex(x1)? as u16)
592            | ((parse_hex(x2)? as u16) << 4)
593            | ((parse_hex(x3)? as u16) << 8)
594            | ((parse_hex(x4)? as u16) << 12),
595    )
596}
597fn scale_u8_to_u16(x: u8) -> u16 {
598    (x as u16) << 8 | (x as u16)
599}
600
601/// Parse an #RGB-style color.
602/// Note: this deviates from XParseColor in order to sensibly interpret #aabbcc as #aaaabbbbcccc
603/// instead of #aa00bb00cc00.
604fn parse_hex_color(data: &[u8]) -> Option<[u16; 4]> {
605    Some(match data {
606        [r, g, b] => [parse_hex1(*r)?, parse_hex1(*g)?, parse_hex1(*b)?, 0xffff],
607        [r2, r1, g2, g1, b2, b1] => [
608            parse_hex2(*r2, *r1)?,
609            parse_hex2(*g2, *g1)?,
610            parse_hex2(*b2, *b1)?,
611            0xffff,
612        ],
613        [r3, r2, r1, g3, g2, g1, b3, b2, b1] => [
614            parse_hex3(*r3, *r2, *r1)?,
615            parse_hex3(*g3, *g2, *g1)?,
616            parse_hex3(*b3, *b2, *b1)?,
617            0xffff,
618        ],
619        [r4, r3, r2, r1, g4, g3, g2, g1, b4, b3, b2, b1] => [
620            parse_hex4(*r4, *r3, *r2, *r1)?,
621            parse_hex4(*g4, *g3, *g2, *g1)?,
622            parse_hex4(*b4, *b3, *b2, *b1)?,
623            0xffff,
624        ],
625        _ => {
626            return None;
627        }
628    })
629}
630
631fn parse_color(data: &[u8]) -> Result<[u16; 4], XpmDecodeError> {
632    if data.starts_with(b"#") {
633        parse_hex_color(&data[1..]).ok_or(XpmDecodeError::BadHexColor)
634    } else {
635        if data == b"none" {
636            return Ok([0, 0, 0, 0]);
637        }
638
639        if let Ok(idx) = x11r6colors::COLORS.binary_search_by(|entry| entry.0.as_bytes().cmp(data))
640        {
641            let entry = x11r6colors::COLORS[idx];
642            Ok([
643                scale_u8_to_u16(entry.1),
644                scale_u8_to_u16(entry.2),
645                scale_u8_to_u16(entry.3),
646                0xffff,
647            ])
648        } else {
649            // At this point, `data` has been validated as alphanumeric ASCII; read_xpm_palette
650            // should ensure its length is <= MAX_COLOR_NAME_LEN
651            assert!(data.len() <= MAX_COLOR_NAME_LEN);
652            let mut tmp = [0u8; MAX_COLOR_NAME_LEN];
653            tmp[..data.len()].copy_from_slice(data);
654            Err(XpmDecodeError::UnknownColor((tmp, data.len() as u8)))
655        }
656    }
657}
658
659/// Read the header of the XPM image and first line
660fn read_xpm_header<R: Iterator<Item = u8>>(
661    r: &mut TextReader<R>,
662) -> Result<XpmHeaderInfo, XpmDecodeError> {
663    // Note: XPM3 header is `/* XPM */`
664    read_fixed_string(r, b"/* XPM */", XpmPart::Header)?;
665    read_to_newline(r, XpmPart::Header)?;
666
667    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
668    read_fixed_string(r, b"static", XpmPart::ArrayStart)?;
669    if skip_whitespace_and_comments(r, XpmPart::ArrayStart)? == 0 {
670        /* need a space or other char between 'static' and 'char' */
671        return Err(XpmDecodeError::Parse(XpmPart::ArrayStart, r.loc()));
672    }
673    read_fixed_string(r, b"char", XpmPart::ArrayStart)?;
674    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
675    read_fixed_string(r, b"*", XpmPart::ArrayStart)?;
676    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
677    read_name(r, XpmPart::ArrayStart)?;
678    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
679    read_fixed_string(r, b"[", XpmPart::ArrayStart)?;
680    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
681    read_fixed_string(r, b"]", XpmPart::ArrayStart)?;
682    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
683    read_fixed_string(r, b"=", XpmPart::ArrayStart)?;
684    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
685    read_fixed_string(r, b"{", XpmPart::ArrayStart)?;
686    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
687
688    /* next: read \" */
689    read_fixed_string(r, b"\"", XpmPart::FirstLine)?;
690
691    // Inside strings, only spaces are allowed for separators
692    let mut int_buf = [0u8; 10]; // 2^32 fits in 10 bytes
693    skip_spaces_and_tabs(r)?; // words separated by space & tabulation chars -- so skip both?
694    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
695    let width = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
696    if width == 0 {
697        return Err(XpmDecodeError::ZeroWidth);
698    }
699
700    read_whitespace_gap(r, XpmPart::FirstLine)?;
701    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
702    let height = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
703    if height == 0 {
704        return Err(XpmDecodeError::ZeroHeight);
705    }
706
707    read_whitespace_gap(r, XpmPart::FirstLine)?;
708    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
709    let ncolors = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
710    read_whitespace_gap(r, XpmPart::FirstLine)?;
711    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
712    let cpp = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
713    skip_spaces_and_tabs(r)?;
714
715    let _hotspot = if let Some(b'"') = r.peek() {
716        // Done
717        None
718    } else {
719        let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
720        let hotspot_x = parse_i32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
721        read_whitespace_gap(r, XpmPart::FirstLine)?;
722        let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
723        let hotspot_y = parse_i32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
724        skip_spaces_and_tabs(r)?;
725
726        // Parse hotspot now.
727        Some((hotspot_x, hotspot_y))
728    };
729    // XPMEXT tags are not supported -- they were essentially never used in practice.
730
731    read_fixed_string(r, b"\"", XpmPart::FirstLine)?;
732    skip_whitespace_and_comments(r, XpmPart::FirstLine)?;
733    read_fixed_string(r, b",", XpmPart::FirstLine)?;
734    skip_whitespace_and_comments(r, XpmPart::FirstLine)?;
735
736    if ncolors == 0 {
737        return Err(XpmDecodeError::ZeroColors);
738    }
739    if cpp == 0 || cpp > 8 {
740        /* cpp larger than 8 is pointless and would not be made by sane encoders:
741         * with hex encoding, it would allow 2^32 distinct colors. */
742        return Err(XpmDecodeError::BadCharsPerColor(cpp));
743    }
744
745    Ok(XpmHeaderInfo {
746        width,
747        height,
748        ncolors,
749        cpp,
750    })
751}
752/// Read the palette portion of the XPM image, stopping just before the first pixel
753fn read_xpm_palette<R: Iterator<Item = u8>>(
754    r: &mut TextReader<R>,
755    info: &XpmHeaderInfo,
756) -> Result<XpmPalette, XpmDecodeError> {
757    assert!(1 <= info.cpp && info.cpp <= 8);
758
759    // Check that color table is sorted
760    assert!(x11r6colors::COLORS.windows(2).all(|p| p[0].0 < p[1].0));
761
762    // Even though the file provides a value for `ncolors`, and memory limits are validated,
763    // do NOT reserve the suggested memory in advance. Dynamically resizing the vector
764    // is negligibly slower, but ensures that the amount of memory allocated is always
765    // bounded by a multiple of the actual file size. Kernel virtual memory optimizations
766    // may hide the performance cost of allocating a 100MB color table from the
767    // application, but such allocations are still expensive even if mostly unused.
768    let mut color_table: Vec<XpmColorCodeEntry> = Vec::new();
769
770    for _col in 0..info.ncolors {
771        read_fixed_string(r, b"\"", XpmPart::Palette)?;
772
773        let mut code = [0_u8; 8];
774        read_all_except_eos(r, &mut code[..info.cpp as usize], XpmPart::Palette)?;
775        read_whitespace_gap(r, XpmPart::Palette)?;
776
777        // Color parsing: XPM color specifications have the form {<key> <color>}+
778        // This is tricky to parse correctly as color names may contain spaces.
779        // Fortunately, the key values are "m", "s", "g4", "g", "c", which will
780        // never be a word within a color name, so one can acquire the entire color
781        // name by parsing until the next key appears or until '"' arrives.
782
783        // Like the X server, this parser does a case-insensitive match on color names.
784        // Unfortunately, there is no general way to handle spaces in names: the color
785        // name database includes variants with spaces for multi-word names that do not
786        // end in a number; e.g. "antiquewhite" has a split variation "antique white",
787        // but "antiquewhite3" does not.
788
789        let mut color_name_buf = [0_u8; MAX_COLOR_NAME_LEN];
790        let mut color_name_len = 0;
791        let mut next_buf = [0_u8; MAX_COLOR_NAME_LEN];
792
793        let mut key: Option<XpmVisual> = None;
794
795        let mut cvis_color = None;
796        loop {
797            if r.peek().unwrap_or(b'"') == b'"' {
798                let Some(ref k) = key else {
799                    // At end of line, must have read a key
800                    return Err(XpmDecodeError::MissingEntry);
801                };
802                if color_name_len == 0 {
803                    // At end of line, must also have read a color to process
804                    return Err(XpmDecodeError::MissingColorAfterKey);
805                }
806
807                let color = handle_key_color(k, &color_name_buf[..color_name_len])?;
808                cvis_color = color.or(cvis_color);
809                break;
810            }
811
812            let next = read_until_whitespace_or_eos(r, &mut next_buf, XpmPart::Palette)?;
813            skip_spaces_and_tabs(r)?;
814
815            let this_key = match &next[..] {
816                b"m" => Some(XpmVisual::Mono),
817                b"s" => Some(XpmVisual::Symbolic),
818                b"g4" => Some(XpmVisual::Grayscale4),
819                b"g" => Some(XpmVisual::Grayscale),
820                b"c" => Some(XpmVisual::Color),
821                _ => None,
822            };
823
824            let Some(ref k) = key else {
825                // No key has been set, is first key-color pair in the line
826                if this_key.is_none() {
827                    // Error: processing non-key value with no preceding key
828                    return Err(XpmDecodeError::MissingKeyBeforeColor);
829                };
830
831                key = this_key;
832                continue;
833            };
834
835            if this_key.is_some() {
836                // End of preceding segment
837                if color_name_len == 0 {
838                    return Err(XpmDecodeError::TwoKeysInARow);
839                }
840
841                let color = handle_key_color(k, &color_name_buf[..color_name_len])?;
842                cvis_color = color.or(cvis_color);
843                color_name_len = 0;
844                key = this_key;
845                continue;
846            }
847
848            // Validate word, case fold it, and concatenate it with the preceding word,
849            // adding a space betweeen words
850            if color_name_len > 0 {
851                if color_name_len < MAX_COLOR_NAME_LEN {
852                    color_name_buf[color_name_len] = b' ';
853                    color_name_len += 1;
854                } else {
855                    return Err(XpmDecodeError::ColorNameTooLong);
856                }
857            }
858            for c in next {
859                if !valid_name_char(*c) {
860                    return Err(XpmDecodeError::InvalidColorName);
861                }
862                // Reduce to lowercase, matching the color name database, to
863                // make regular string comparisons be case-insensitive
864                if color_name_len < MAX_COLOR_NAME_LEN {
865                    color_name_buf[color_name_len] = fold_to_lower(*c);
866                    color_name_len += 1;
867                } else {
868                    return Err(XpmDecodeError::ColorNameTooLong);
869                }
870            }
871        }
872
873        let Some(color) = cvis_color else {
874            return Err(XpmDecodeError::NoColorModeColorSpecified);
875        };
876
877        color_table.push(XpmColorCodeEntry {
878            code: u64::from_le_bytes(code),
879            value: color,
880        });
881
882        read_fixed_string(r, b"\"", XpmPart::Palette)?;
883        skip_whitespace_and_comments(r, XpmPart::Palette)?;
884        read_fixed_string(r, b",", XpmPart::Palette)?;
885        skip_whitespace_and_comments(r, XpmPart::Palette)?;
886    }
887
888    // Sort table and check for duplicates
889    color_table.sort_unstable_by(|x, y| x.code.cmp(&y.code));
890    for w in color_table.windows(2) {
891        if w[0].code.cmp(&w[1].code) != Ordering::Less {
892            return Err(XpmDecodeError::DuplicateCode);
893        }
894    }
895
896    read_fixed_string(r, b"\"", XpmPart::Body)?;
897
898    Ok(XpmPalette { table: color_table })
899}
900/// Read a single pixel from within the main image area
901fn read_xpm_pixel<R: Iterator<Item = u8>>(
902    r: &mut TextReader<R>,
903    info: &XpmHeaderInfo,
904    palette: &XpmPalette,
905    chunk: &mut [u8; 8],
906) -> Result<(), XpmDecodeError> {
907    let mut code = [0_u8; 8];
908    read_all_except_eos(r, &mut code[..info.cpp as usize], XpmPart::Palette)?;
909    let code = u64::from_le_bytes(code);
910
911    let Ok(index) = palette
912        .table
913        .binary_search_by(|entry| entry.code.cmp(&code))
914    else {
915        return Err(XpmDecodeError::UnknownCode);
916    };
917
918    let color = palette.table[index].value;
919    // ColorType::Rgba16 is currently native endian, R,G,B,A channel order
920    chunk[0..2].copy_from_slice(&color[0].to_ne_bytes());
921    chunk[2..4].copy_from_slice(&color[1].to_ne_bytes());
922    chunk[4..6].copy_from_slice(&color[2].to_ne_bytes());
923    chunk[6..8].copy_from_slice(&color[3].to_ne_bytes());
924    Ok(())
925}
926/// Read the end of this row of the XPM image body and the start of the next.
927/// Should only be called between rows, and not after the last one
928fn read_xpm_row_transition<R: Iterator<Item = u8>>(
929    r: &mut TextReader<R>,
930) -> Result<(), XpmDecodeError> {
931    // End of this line
932    read_fixed_string(r, b"\"", XpmPart::Body)?;
933
934    skip_whitespace_and_comments(r, XpmPart::Body)?;
935    read_fixed_string(r, b",", XpmPart::Body)?;
936    skip_whitespace_and_comments(r, XpmPart::Body)?;
937    // Start of next line
938    read_fixed_string(r, b"\"", XpmPart::Body)?;
939    Ok(())
940}
941/// Read the end of the XPM image
942fn read_xpm_trailing<R: Iterator<Item = u8>>(r: &mut TextReader<R>) -> Result<(), XpmDecodeError> {
943    // Read end of last line
944    read_fixed_string(r, b"\"", XpmPart::Body)?;
945
946    // Read optional comma, followed by final };
947    skip_whitespace_and_comments(r, XpmPart::Trailing)?;
948    let next = read_byte(r, XpmPart::Trailing)?;
949    if next == b',' {
950        skip_whitespace_and_comments(r, XpmPart::Trailing)?;
951        read_fixed_string(r, b"}", XpmPart::Trailing)?;
952    } else if next != b'}' {
953        return Err(XpmDecodeError::Parse(XpmPart::Trailing, r.loc()));
954    }
955    skip_whitespace_and_comments(r, XpmPart::Trailing)?;
956    read_fixed_string(r, b";", XpmPart::Trailing)?;
957
958    skip_whitespace_and_comments(r, XpmPart::AfterEnd)?;
959    if r.next().is_some() {
960        // File has unexpected trailing contents.
961        Err(XpmDecodeError::Parse(XpmPart::AfterEnd, r.loc()))
962    } else {
963        Ok(())
964    }
965}
966
967impl<R> XpmDecoder<R>
968where
969    R: BufRead,
970{
971    /// Create a new [XpmDecoder].
972    pub fn new(reader: R) -> Result<XpmDecoder<R>, ImageError> {
973        let mut r = TextReader::new(IoAdapter {
974            reader: reader.bytes(),
975            error: None,
976        });
977
978        let info = read_xpm_header(&mut r).apply_after(&mut r.inner.error)?;
979
980        Ok(XpmDecoder { r, info })
981    }
982}
983
984/// Parse color, returning it if the key is also XpmVisual::Color
985fn handle_key_color(key: &XpmVisual, color: &[u8]) -> Result<Option<[u16; 4]>, XpmDecodeError> {
986    if matches!(key, XpmVisual::Symbolic) {
987        return Ok(None);
988    }
989    let color = parse_color(color)?;
990    if matches!(key, XpmVisual::Color) {
991        Ok(Some(color))
992    } else {
993        Ok(None)
994    }
995}
996
997impl<R: BufRead> ImageDecoder for XpmDecoder<R> {
998    fn dimensions(&self) -> (u32, u32) {
999        (self.info.width, self.info.height)
1000    }
1001    fn color_type(&self) -> ColorType {
1002        // note: some images specify 16-bpc colors, and fully transparent pixels are possible,
1003        // so RGBA16 is needed to handle all possible cases
1004        ColorType::Rgba16
1005    }
1006    fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()>
1007    where
1008        Self: Sized,
1009    {
1010        assert!(1 <= self.info.cpp && self.info.cpp <= 8);
1011
1012        let palette =
1013            read_xpm_palette(&mut self.r, &self.info).apply_after(&mut self.r.inner.error)?;
1014
1015        // Read main image contents
1016        let stride = (self.info.width as usize).checked_mul(8).unwrap();
1017        for (i, row) in buf.chunks_exact_mut(stride).enumerate() {
1018            for chunk in row.chunks_exact_mut(8) {
1019                read_xpm_pixel(&mut self.r, &self.info, &palette, chunk.try_into().unwrap())
1020                    .apply_after(&mut self.r.inner.error)?;
1021            }
1022
1023            if i >= (self.info.height - 1) as usize {
1024                // Last row,
1025            } else {
1026                read_xpm_row_transition(&mut self.r).apply_after(&mut self.r.inner.error)?;
1027            }
1028        }
1029
1030        read_xpm_trailing(&mut self.r).apply_after(&mut self.r.inner.error)?;
1031
1032        Ok(())
1033    }
1034    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
1035        (*self).read_image(buf)
1036    }
1037
1038    fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
1039        limits.check_support(&LimitSupport::default())?;
1040        let (width, height) = self.dimensions();
1041        limits.check_dimensions(width, height)?;
1042
1043        let max_pixels = u64::from(self.info.width) * u64::from(self.info.height);
1044        let max_image_bytes =
1045            max_pixels
1046                .checked_mul(8)
1047                .ok_or(ImageError::Limits(LimitError::from_kind(
1048                    LimitErrorKind::DimensionError,
1049                )))?;
1050
1051        let max_table_bytes = (self.info.ncolors as u64) * (size_of::<XpmColorCodeEntry>() as u64);
1052        let max_bytes = max_image_bytes
1053            .checked_add(max_table_bytes)
1054            .ok_or(ImageError::Limits(LimitError::from_kind(
1055                LimitErrorKind::InsufficientMemory,
1056            )))?;
1057
1058        let max_alloc = limits.max_alloc.unwrap_or(u64::MAX);
1059        if max_alloc < max_bytes {
1060            return Err(ImageError::Limits(LimitError::from_kind(
1061                LimitErrorKind::InsufficientMemory,
1062            )));
1063        }
1064        Ok(())
1065    }
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070    use super::*;
1071
1072    #[test]
1073    fn image_missing_body() {
1074        let data = b"/* XPM */
1075static char *test[] = {
1076\"20 5 10 1\",
1077};
1078";
1079        let decoder = XpmDecoder::new(&data[..]).unwrap();
1080        let mut image = vec![0; decoder.total_bytes() as usize];
1081        assert!(decoder.read_image(&mut image).is_err());
1082    }
1083
1084    #[test]
1085    fn invalid_color_name() {
1086        let data = b"/* XPM */
1087static char *test[] = {
1088    \"1 1 1 1\",
1089    \"  c Antique White1\",
1090    \" \",
1091};";
1092        let decoder = XpmDecoder::new(&data[..]).unwrap();
1093        let mut image = vec![0; decoder.total_bytes() as usize];
1094        assert!(decoder.read_image(&mut image).is_err());
1095    }
1096
1097    #[test]
1098    fn trailing_semicolon_required() {
1099        let data = b"/* XPM */
1100        static char *test[] = {
1101        \"1 1 1 1\",
1102        \"  c none\",
1103        \" \",
1104    };";
1105        let decoder = XpmDecoder::new(&data[..data.len() - 1]).unwrap();
1106        let mut image = vec![0; decoder.total_bytes() as usize];
1107        assert!(decoder.read_image(&mut image).is_err());
1108
1109        let decoder = XpmDecoder::new(&data[..]).unwrap();
1110        let mut image = vec![0; decoder.total_bytes() as usize];
1111        assert!(decoder.read_image(&mut image).is_ok());
1112    }
1113}