Skip to main content

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 a C keyword into the buffer and returns a slice of the buffer for the
330/// keyword.
331///
332/// The only allowed characters are a-z, A-Z, and _. Reading will stop if
333/// a non-allowed character or EOF is reached. If the buffer is too small, an
334/// error will be returned.
335fn read_keyword<'buf, R: Iterator<Item = u8>>(
336    r: &mut TextReader<R>,
337    buf: &'buf mut [u8],
338    part: XpmPart,
339) -> Result<&'buf [u8], XpmDecodeError> {
340    let mut len = 0;
341
342    while let Some(b) = r.peek() {
343        if matches!(b, b'_' | b'a'..=b'z' | b'A'..=b'Z') {
344            if len >= buf.len() {
345                // identifier too long
346                return Err(XpmDecodeError::Parse(part, r.loc()));
347            }
348            buf[len] = b;
349            len += 1;
350            r.next();
351        } else {
352            break;
353        }
354    }
355
356    Ok(&buf[..len])
357}
358/// Read precisely the string `s` from `r`, or error.
359fn read_fixed_string<R: Iterator<Item = u8>>(
360    r: &mut TextReader<R>,
361    s: &[u8],
362    part: XpmPart,
363) -> Result<(), XpmDecodeError> {
364    for c in s {
365        if let Some(b) = r.next() {
366            if b != *c {
367                return Err(XpmDecodeError::Parse(part, r.loc()));
368            }
369        } else {
370            return Err(XpmDecodeError::Parse(part, r.loc()));
371        };
372    }
373    Ok(())
374}
375// Read a single byte
376fn read_byte<R: Iterator<Item = u8>>(
377    r: &mut TextReader<R>,
378    part: XpmPart,
379) -> Result<u8, XpmDecodeError> {
380    match r.next() {
381        None => Err(XpmDecodeError::Parse(part, r.loc())),
382        Some(b) => Ok(b),
383    }
384}
385
386/// Read a mixture of ' ' and '\t'. At least one character must be read.
387// Other whitespace characters are not permitted.
388fn read_whitespace_gap<R: Iterator<Item = u8>>(
389    r: &mut TextReader<R>,
390    part: XpmPart,
391) -> Result<(), XpmDecodeError> {
392    let b = read_byte(r, part)?;
393    if !(b == b' ' || b == b'\t') {
394        return Err(XpmDecodeError::Parse(part, r.loc()));
395    }
396    while let Some(b) = r.peek() {
397        if b == b' ' || b == b'\t' {
398            r.next();
399            continue;
400        } else {
401            return Ok(());
402        }
403    }
404    Ok(())
405}
406
407/// Read a mixture of ' ', '\t', '\n', and C-style /* comments */.
408/// This will error if it sees a / without following *
409fn skip_whitespace_and_comments<R: Iterator<Item = u8>>(
410    r: &mut TextReader<R>,
411    part: XpmPart,
412) -> Result<usize, XpmDecodeError> {
413    let mut nbytes = 0;
414
415    // `has_first_char`: If out of comment, has / ; if in comment, has *
416    let mut has_first_char = false;
417    let mut in_comment = false;
418
419    while let Some(b) = r.peek() {
420        if !in_comment {
421            if has_first_char {
422                if b != b'*' {
423                    return Err(XpmDecodeError::Parse(part, r.loc()));
424                } else {
425                    in_comment = true;
426                    has_first_char = false;
427                }
428            }
429            if b == b'/' {
430                has_first_char = true;
431            }
432        }
433        if b == b' ' || b == b'\t' || b == b'\n' || b == b'/' || in_comment {
434            if in_comment {
435                if has_first_char && b == b'/' {
436                    in_comment = false;
437                }
438                has_first_char = b == b'*';
439            }
440            nbytes += 1;
441            r.next();
442            continue;
443        } else {
444            break;
445        }
446    }
447    if !in_comment && has_first_char {
448        // Parsed up to a / but did not find *
449        return Err(XpmDecodeError::Parse(part, r.loc()));
450    }
451
452    Ok(nbytes)
453}
454
455/// Skips at least one whitespace or comment.
456fn skip_non_empty_whitespace_and_comments<R: Iterator<Item = u8>>(
457    r: &mut TextReader<R>,
458    part: XpmPart,
459) -> Result<(), XpmDecodeError> {
460    let spaces = skip_whitespace_and_comments(r, part)?;
461    if spaces == 0 {
462        return Err(XpmDecodeError::Parse(part, r.loc()));
463    }
464    Ok(())
465}
466
467fn skip_spaces_and_tabs<R: Iterator<Item = u8>>(
468    r: &mut TextReader<R>,
469) -> Result<usize, XpmDecodeError> {
470    let mut nbytes = 0;
471    while let Some(b) = r.peek() {
472        if b == b' ' || b == b'\t' {
473            nbytes += 1;
474            r.next();
475            continue;
476        } else {
477            break;
478        }
479    }
480    Ok(nbytes)
481}
482
483/// Read a mixture of ' ' and '\t', until reading '\n'.
484fn read_to_newline<R: Iterator<Item = u8>>(
485    r: &mut TextReader<R>,
486    part: XpmPart,
487) -> Result<(), XpmDecodeError> {
488    while let Some(b) = r.peek() {
489        if b == b' ' || b == b'\t' {
490            r.next();
491            continue;
492        } else {
493            break;
494        }
495    }
496    if read_byte(r, part)? != b'\n' {
497        Err(XpmDecodeError::Parse(part, r.loc()))
498    } else {
499        Ok(())
500    }
501}
502/// Read token into the buffer until the buffer size is exceeded, or ' ' or '\t' or '"' is found
503/// \ characters are forbidden. Returns the region of data read.
504fn read_until_whitespace_or_eos<'a, R: Iterator<Item = u8>>(
505    r: &mut TextReader<R>,
506    buf: &'a mut [u8],
507    part: XpmPart,
508) -> Result<&'a mut [u8], XpmDecodeError> {
509    let mut len = 0;
510    while let Some(b) = r.peek() {
511        if b == b' ' || b == b'\t' || b == b'"' {
512            return Ok(&mut buf[..len]);
513        } else if b == b'\\' {
514            r.next();
515            return Err(XpmDecodeError::Parse(part, r.loc()));
516        } else {
517            if len >= buf.len() {
518                // identifier is too long
519                return Err(XpmDecodeError::Parse(part, r.loc()));
520            }
521            buf[len] = b;
522            len += 1;
523            r.next();
524        }
525    }
526    Ok(&mut buf[..len])
527}
528
529/// Read fixed length token into the buffer. Errors if file ends, or " or \ is found.
530fn read_all_except_eos<R: Iterator<Item = u8>>(
531    r: &mut TextReader<R>,
532    buf: &mut [u8],
533    part: XpmPart,
534) -> Result<(), XpmDecodeError> {
535    let mut len = 0;
536    while let Some(b) = r.peek() {
537        if b == b'"' || b == b'\\' {
538            r.next();
539            return Err(XpmDecodeError::Parse(part, r.loc()));
540        } else {
541            buf[len] = b;
542            len += 1;
543            r.next();
544            if len >= buf.len() {
545                return Ok(());
546            }
547        }
548    }
549    Err(XpmDecodeError::Parse(part, r.loc()))
550}
551
552/// Read the name portion of the file (but do not validate it, because some old files
553/// may put invalid characters here (like "." and "-") or use 8-bit character sets instead
554/// of Unicode.)
555fn read_name<R: Iterator<Item = u8>>(
556    r: &mut TextReader<R>,
557    part: XpmPart,
558) -> Result<(), XpmDecodeError> {
559    let mut empty = true;
560    while let Some(b) = r.peek() {
561        match b {
562            b'/' | b' ' | b'\t' | b'\n' | b'[' => {
563                break;
564            }
565            _ => (),
566        }
567        r.next();
568        empty = false;
569    }
570    if empty {
571        return Err(XpmDecodeError::Parse(part, r.loc()));
572    }
573
574    Ok(())
575}
576
577/// Parse string into integer, rejecting leading + and leading zeros
578fn parse_i32(data: &[u8]) -> Option<i32> {
579    if data.starts_with(b"-") {
580        (-(parse_u32(&data[1..])? as i64)).try_into().ok()
581    } else {
582        parse_u32(data)?.try_into().ok()
583    }
584}
585
586/// Parse string into unsigned integer, rejecting leading + and leading zeros
587fn parse_u32(data: &[u8]) -> Option<u32> {
588    let Some(c1) = data.first() else {
589        // Reject empty string
590        return None;
591    };
592    if *c1 == b'0' && data.len() > 1 {
593        // Reject leading zeros unless value is exactly zero
594        return None;
595    }
596    let mut x: u32 = 0;
597    for c in data {
598        if b'0' <= *c && *c <= b'9' {
599            x = x.checked_mul(10)?.checked_add((*c - b'0') as u32)?;
600        } else {
601            return None;
602        }
603    }
604    Some(x)
605}
606fn parse_hex(b: u8) -> Option<u8> {
607    match b {
608        b'0'..=b'9' => Some(b - b'0'),
609        b'A'..=b'F' => Some(b - b'A' + 10),
610        b'a'..=b'f' => Some(b - b'a' + 10),
611        _ => None,
612    }
613}
614fn parse_hex1(x1: u8) -> Option<u16> {
615    let x = parse_hex(x1)? as u16;
616    Some(x | (x << 4) | (x << 8) | (x << 12))
617}
618fn parse_hex2(x2: u8, x1: u8) -> Option<u16> {
619    let x = ((parse_hex(x2)? as u16) << 4) | (parse_hex(x1)? as u16);
620    Some(x | (x << 8))
621}
622fn parse_hex3(x3: u8, x2: u8, x1: u8) -> Option<u16> {
623    let x =
624        ((parse_hex(x3)? as u16) << 8) | ((parse_hex(x2)? as u16) << 4) | (parse_hex(x1)? as u16);
625    // There are four reasonable approaches to converting 12-bit to 16-bit,
626    // round down, round nearest, round up, and round fast
627    // (x*65535)/4095, (x*65535+2047)/4095, (x*65535+4094)/4095, and (x<<4)|(x>>8).
628    Some((((x as u32) * 65535 + 2047) / 4095) as u16)
629}
630fn parse_hex4(x4: u8, x3: u8, x2: u8, x1: u8) -> Option<u16> {
631    Some(
632        (parse_hex(x1)? as u16)
633            | ((parse_hex(x2)? as u16) << 4)
634            | ((parse_hex(x3)? as u16) << 8)
635            | ((parse_hex(x4)? as u16) << 12),
636    )
637}
638fn scale_u8_to_u16(x: u8) -> u16 {
639    (x as u16) << 8 | (x as u16)
640}
641
642/// Parse an #RGB-style color.
643/// Note: this deviates from XParseColor in order to sensibly interpret #aabbcc as #aaaabbbbcccc
644/// instead of #aa00bb00cc00.
645fn parse_hex_color(data: &[u8]) -> Option<[u16; 4]> {
646    Some(match data {
647        [r, g, b] => [parse_hex1(*r)?, parse_hex1(*g)?, parse_hex1(*b)?, 0xffff],
648        [r2, r1, g2, g1, b2, b1] => [
649            parse_hex2(*r2, *r1)?,
650            parse_hex2(*g2, *g1)?,
651            parse_hex2(*b2, *b1)?,
652            0xffff,
653        ],
654        [r3, r2, r1, g3, g2, g1, b3, b2, b1] => [
655            parse_hex3(*r3, *r2, *r1)?,
656            parse_hex3(*g3, *g2, *g1)?,
657            parse_hex3(*b3, *b2, *b1)?,
658            0xffff,
659        ],
660        [r4, r3, r2, r1, g4, g3, g2, g1, b4, b3, b2, b1] => [
661            parse_hex4(*r4, *r3, *r2, *r1)?,
662            parse_hex4(*g4, *g3, *g2, *g1)?,
663            parse_hex4(*b4, *b3, *b2, *b1)?,
664            0xffff,
665        ],
666        _ => {
667            return None;
668        }
669    })
670}
671
672fn parse_color(data: &[u8]) -> Result<[u16; 4], XpmDecodeError> {
673    if data.starts_with(b"#") {
674        parse_hex_color(&data[1..]).ok_or(XpmDecodeError::BadHexColor)
675    } else {
676        if data == b"none" {
677            return Ok([0, 0, 0, 0]);
678        }
679
680        if let Ok(idx) = x11r6colors::COLORS.binary_search_by(|entry| entry.0.as_bytes().cmp(data))
681        {
682            let entry = x11r6colors::COLORS[idx];
683            Ok([
684                scale_u8_to_u16(entry.1),
685                scale_u8_to_u16(entry.2),
686                scale_u8_to_u16(entry.3),
687                0xffff,
688            ])
689        } else {
690            // At this point, `data` has been validated as alphanumeric ASCII; read_xpm_palette
691            // should ensure its length is <= MAX_COLOR_NAME_LEN
692            assert!(data.len() <= MAX_COLOR_NAME_LEN);
693            let mut tmp = [0u8; MAX_COLOR_NAME_LEN];
694            tmp[..data.len()].copy_from_slice(data);
695            Err(XpmDecodeError::UnknownColor((tmp, data.len() as u8)))
696        }
697    }
698}
699
700/// Read the header of the XPM image and first line
701fn read_xpm_header<R: Iterator<Item = u8>>(
702    r: &mut TextReader<R>,
703) -> Result<XpmHeaderInfo, XpmDecodeError> {
704    let keyword_buf = &mut [0u8; 16];
705
706    // Note: XPM3 header is `/* XPM */`
707    read_fixed_string(r, b"/* XPM */", XpmPart::Header)?;
708    read_to_newline(r, XpmPart::Header)?;
709
710    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
711    read_fixed_string(r, b"static", XpmPart::ArrayStart)?;
712    skip_non_empty_whitespace_and_comments(r, XpmPart::ArrayStart)?;
713
714    // There may be an optional "const" keyword before "char".
715    // This is NOT part of the XPM 3 specification.
716    // This was added by ImageMagick 7.1.0-4 in mid 2021
717    // (https://github.com/ImageMagick/ImageMagick/commit/e7d3e182b72ff9b2c3ea1c9aa0f14d69cc968ba7)
718    // to help with C++ compiler warnings (https://github.com/ImageMagick/ImageMagick/issues/3951).
719    let keyword = read_keyword(r, keyword_buf, XpmPart::ArrayStart)?;
720    match keyword {
721        b"const" => {
722            skip_non_empty_whitespace_and_comments(r, XpmPart::ArrayStart)?;
723            read_fixed_string(r, b"char", XpmPart::ArrayStart)?;
724        }
725        b"char" => (),
726        _ => return Err(XpmDecodeError::Parse(XpmPart::ArrayStart, r.loc())),
727    }
728
729    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
730    read_fixed_string(r, b"*", XpmPart::ArrayStart)?;
731    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
732    read_name(r, XpmPart::ArrayStart)?;
733    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
734    read_fixed_string(r, b"[", XpmPart::ArrayStart)?;
735    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
736    read_fixed_string(r, b"]", XpmPart::ArrayStart)?;
737    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
738    read_fixed_string(r, b"=", XpmPart::ArrayStart)?;
739    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
740    read_fixed_string(r, b"{", XpmPart::ArrayStart)?;
741    skip_whitespace_and_comments(r, XpmPart::ArrayStart)?;
742
743    /* next: read \" */
744    read_fixed_string(r, b"\"", XpmPart::FirstLine)?;
745
746    // Inside strings, only spaces are allowed for separators
747    let mut int_buf = [0u8; 10]; // 2^32 fits in 10 bytes
748    skip_spaces_and_tabs(r)?; // words separated by space & tabulation chars -- so skip both?
749    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
750    let width = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
751    if width == 0 {
752        return Err(XpmDecodeError::ZeroWidth);
753    }
754
755    read_whitespace_gap(r, XpmPart::FirstLine)?;
756    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
757    let height = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
758    if height == 0 {
759        return Err(XpmDecodeError::ZeroHeight);
760    }
761
762    read_whitespace_gap(r, XpmPart::FirstLine)?;
763    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
764    let ncolors = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
765    read_whitespace_gap(r, XpmPart::FirstLine)?;
766    let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
767    let cpp = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
768    skip_spaces_and_tabs(r)?;
769
770    let _hotspot = if let Some(b'"') = r.peek() {
771        // Done
772        None
773    } else {
774        let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
775        let hotspot_x = parse_i32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
776        read_whitespace_gap(r, XpmPart::FirstLine)?;
777        let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?;
778        let hotspot_y = parse_i32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?;
779        skip_spaces_and_tabs(r)?;
780
781        // Parse hotspot now.
782        Some((hotspot_x, hotspot_y))
783    };
784    // XPMEXT tags are not supported -- they were essentially never used in practice.
785
786    read_fixed_string(r, b"\"", XpmPart::FirstLine)?;
787    skip_whitespace_and_comments(r, XpmPart::FirstLine)?;
788    read_fixed_string(r, b",", XpmPart::FirstLine)?;
789    skip_whitespace_and_comments(r, XpmPart::FirstLine)?;
790
791    if ncolors == 0 {
792        return Err(XpmDecodeError::ZeroColors);
793    }
794    if cpp == 0 || cpp > 8 {
795        /* cpp larger than 8 is pointless and would not be made by sane encoders:
796         * with hex encoding, it would allow 2^32 distinct colors. */
797        return Err(XpmDecodeError::BadCharsPerColor(cpp));
798    }
799
800    Ok(XpmHeaderInfo {
801        width,
802        height,
803        ncolors,
804        cpp,
805    })
806}
807/// Read the palette portion of the XPM image, stopping just before the first pixel
808fn read_xpm_palette<R: Iterator<Item = u8>>(
809    r: &mut TextReader<R>,
810    info: &XpmHeaderInfo,
811) -> Result<XpmPalette, XpmDecodeError> {
812    assert!(1 <= info.cpp && info.cpp <= 8);
813
814    // Check that color table is sorted
815    assert!(x11r6colors::COLORS.windows(2).all(|p| p[0].0 < p[1].0));
816
817    // Even though the file provides a value for `ncolors`, and memory limits are validated,
818    // do NOT reserve the suggested memory in advance. Dynamically resizing the vector
819    // is negligibly slower, but ensures that the amount of memory allocated is always
820    // bounded by a multiple of the actual file size. Kernel virtual memory optimizations
821    // may hide the performance cost of allocating a 100MB color table from the
822    // application, but such allocations are still expensive even if mostly unused.
823    let mut color_table: Vec<XpmColorCodeEntry> = Vec::new();
824
825    for _col in 0..info.ncolors {
826        read_fixed_string(r, b"\"", XpmPart::Palette)?;
827
828        let mut code = [0_u8; 8];
829        read_all_except_eos(r, &mut code[..info.cpp as usize], XpmPart::Palette)?;
830        read_whitespace_gap(r, XpmPart::Palette)?;
831
832        // Color parsing: XPM color specifications have the form {<key> <color>}+
833        // This is tricky to parse correctly as color names may contain spaces.
834        // Fortunately, the key values are "m", "s", "g4", "g", "c", which will
835        // never be a word within a color name, so one can acquire the entire color
836        // name by parsing until the next key appears or until '"' arrives.
837
838        // Like the X server, this parser does a case-insensitive match on color names.
839        // Unfortunately, there is no general way to handle spaces in names: the color
840        // name database includes variants with spaces for multi-word names that do not
841        // end in a number; e.g. "antiquewhite" has a split variation "antique white",
842        // but "antiquewhite3" does not.
843
844        let mut color_name_buf = [0_u8; MAX_COLOR_NAME_LEN];
845        let mut color_name_len = 0;
846        let mut next_buf = [0_u8; MAX_COLOR_NAME_LEN];
847
848        let mut key: Option<XpmVisual> = None;
849
850        let mut cvis_color = None;
851        loop {
852            if r.peek().unwrap_or(b'"') == b'"' {
853                let Some(ref k) = key else {
854                    // At end of line, must have read a key
855                    return Err(XpmDecodeError::MissingEntry);
856                };
857                if color_name_len == 0 {
858                    // At end of line, must also have read a color to process
859                    return Err(XpmDecodeError::MissingColorAfterKey);
860                }
861
862                let color = handle_key_color(k, &color_name_buf[..color_name_len])?;
863                cvis_color = color.or(cvis_color);
864                break;
865            }
866
867            let next = read_until_whitespace_or_eos(r, &mut next_buf, XpmPart::Palette)?;
868            skip_spaces_and_tabs(r)?;
869
870            let this_key = match &next[..] {
871                b"m" => Some(XpmVisual::Mono),
872                b"s" => Some(XpmVisual::Symbolic),
873                b"g4" => Some(XpmVisual::Grayscale4),
874                b"g" => Some(XpmVisual::Grayscale),
875                b"c" => Some(XpmVisual::Color),
876                _ => None,
877            };
878
879            let Some(ref k) = key else {
880                // No key has been set, is first key-color pair in the line
881                if this_key.is_none() {
882                    // Error: processing non-key value with no preceding key
883                    return Err(XpmDecodeError::MissingKeyBeforeColor);
884                };
885
886                key = this_key;
887                continue;
888            };
889
890            if this_key.is_some() {
891                // End of preceding segment
892                if color_name_len == 0 {
893                    return Err(XpmDecodeError::TwoKeysInARow);
894                }
895
896                let color = handle_key_color(k, &color_name_buf[..color_name_len])?;
897                cvis_color = color.or(cvis_color);
898                color_name_len = 0;
899                key = this_key;
900                continue;
901            }
902
903            // Validate word, case fold it, and concatenate it with the preceding word,
904            // adding a space betweeen words
905            if color_name_len > 0 {
906                if color_name_len < MAX_COLOR_NAME_LEN {
907                    color_name_buf[color_name_len] = b' ';
908                    color_name_len += 1;
909                } else {
910                    return Err(XpmDecodeError::ColorNameTooLong);
911                }
912            }
913            for c in next {
914                if !valid_name_char(*c) {
915                    return Err(XpmDecodeError::InvalidColorName);
916                }
917                // Reduce to lowercase, matching the color name database, to
918                // make regular string comparisons be case-insensitive
919                if color_name_len < MAX_COLOR_NAME_LEN {
920                    color_name_buf[color_name_len] = fold_to_lower(*c);
921                    color_name_len += 1;
922                } else {
923                    return Err(XpmDecodeError::ColorNameTooLong);
924                }
925            }
926        }
927
928        let Some(color) = cvis_color else {
929            return Err(XpmDecodeError::NoColorModeColorSpecified);
930        };
931
932        color_table.push(XpmColorCodeEntry {
933            code: u64::from_le_bytes(code),
934            value: color,
935        });
936
937        read_fixed_string(r, b"\"", XpmPart::Palette)?;
938        skip_whitespace_and_comments(r, XpmPart::Palette)?;
939        read_fixed_string(r, b",", XpmPart::Palette)?;
940        skip_whitespace_and_comments(r, XpmPart::Palette)?;
941    }
942
943    // Sort table and check for duplicates
944    color_table.sort_unstable_by_key(|x| x.code);
945    for w in color_table.windows(2) {
946        if w[0].code.cmp(&w[1].code) != Ordering::Less {
947            return Err(XpmDecodeError::DuplicateCode);
948        }
949    }
950
951    read_fixed_string(r, b"\"", XpmPart::Body)?;
952
953    Ok(XpmPalette { table: color_table })
954}
955/// Read a single pixel from within the main image area
956fn read_xpm_pixel<R: Iterator<Item = u8>>(
957    r: &mut TextReader<R>,
958    info: &XpmHeaderInfo,
959    palette: &XpmPalette,
960    chunk: &mut [u8; 8],
961) -> Result<(), XpmDecodeError> {
962    let mut code = [0_u8; 8];
963    read_all_except_eos(r, &mut code[..info.cpp as usize], XpmPart::Palette)?;
964    let code = u64::from_le_bytes(code);
965
966    let Ok(index) = palette
967        .table
968        .binary_search_by(|entry| entry.code.cmp(&code))
969    else {
970        return Err(XpmDecodeError::UnknownCode);
971    };
972
973    let color = palette.table[index].value;
974    // ColorType::Rgba16 is currently native endian, R,G,B,A channel order
975    chunk[0..2].copy_from_slice(&color[0].to_ne_bytes());
976    chunk[2..4].copy_from_slice(&color[1].to_ne_bytes());
977    chunk[4..6].copy_from_slice(&color[2].to_ne_bytes());
978    chunk[6..8].copy_from_slice(&color[3].to_ne_bytes());
979    Ok(())
980}
981/// Read the end of this row of the XPM image body and the start of the next.
982/// Should only be called between rows, and not after the last one
983fn read_xpm_row_transition<R: Iterator<Item = u8>>(
984    r: &mut TextReader<R>,
985) -> Result<(), XpmDecodeError> {
986    // End of this line
987    read_fixed_string(r, b"\"", XpmPart::Body)?;
988
989    skip_whitespace_and_comments(r, XpmPart::Body)?;
990    read_fixed_string(r, b",", XpmPart::Body)?;
991    skip_whitespace_and_comments(r, XpmPart::Body)?;
992    // Start of next line
993    read_fixed_string(r, b"\"", XpmPart::Body)?;
994    Ok(())
995}
996/// Read the end of the XPM image
997fn read_xpm_trailing<R: Iterator<Item = u8>>(r: &mut TextReader<R>) -> Result<(), XpmDecodeError> {
998    // Read end of last line
999    read_fixed_string(r, b"\"", XpmPart::Body)?;
1000
1001    // Read optional comma, followed by final };
1002    skip_whitespace_and_comments(r, XpmPart::Trailing)?;
1003    let next = read_byte(r, XpmPart::Trailing)?;
1004    if next == b',' {
1005        skip_whitespace_and_comments(r, XpmPart::Trailing)?;
1006        read_fixed_string(r, b"}", XpmPart::Trailing)?;
1007    } else if next != b'}' {
1008        return Err(XpmDecodeError::Parse(XpmPart::Trailing, r.loc()));
1009    }
1010    skip_whitespace_and_comments(r, XpmPart::Trailing)?;
1011    read_fixed_string(r, b";", XpmPart::Trailing)?;
1012
1013    skip_whitespace_and_comments(r, XpmPart::AfterEnd)?;
1014    if r.next().is_some() {
1015        // File has unexpected trailing contents.
1016        Err(XpmDecodeError::Parse(XpmPart::AfterEnd, r.loc()))
1017    } else {
1018        Ok(())
1019    }
1020}
1021
1022impl<R> XpmDecoder<R>
1023where
1024    R: BufRead,
1025{
1026    /// Create a new [XpmDecoder].
1027    pub fn new(reader: R) -> Result<XpmDecoder<R>, ImageError> {
1028        let mut r = TextReader::new(IoAdapter {
1029            reader: reader.bytes(),
1030            error: None,
1031        });
1032
1033        let info = read_xpm_header(&mut r).apply_after(&mut r.inner.error)?;
1034
1035        Ok(XpmDecoder { r, info })
1036    }
1037}
1038
1039/// Parse color, returning it if the key is also XpmVisual::Color
1040fn handle_key_color(key: &XpmVisual, color: &[u8]) -> Result<Option<[u16; 4]>, XpmDecodeError> {
1041    if matches!(key, XpmVisual::Symbolic) {
1042        return Ok(None);
1043    }
1044    let color = parse_color(color)?;
1045    if matches!(key, XpmVisual::Color) {
1046        Ok(Some(color))
1047    } else {
1048        Ok(None)
1049    }
1050}
1051
1052impl<R: BufRead> ImageDecoder for XpmDecoder<R> {
1053    fn dimensions(&self) -> (u32, u32) {
1054        (self.info.width, self.info.height)
1055    }
1056    fn color_type(&self) -> ColorType {
1057        // note: some images specify 16-bpc colors, and fully transparent pixels are possible,
1058        // so RGBA16 is needed to handle all possible cases
1059        ColorType::Rgba16
1060    }
1061    fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()>
1062    where
1063        Self: Sized,
1064    {
1065        assert!(1 <= self.info.cpp && self.info.cpp <= 8);
1066
1067        let palette =
1068            read_xpm_palette(&mut self.r, &self.info).apply_after(&mut self.r.inner.error)?;
1069
1070        // Read main image contents
1071        let stride = (self.info.width as usize).checked_mul(8).unwrap();
1072        for (i, row) in buf.chunks_exact_mut(stride).enumerate() {
1073            for chunk in row.chunks_exact_mut(8) {
1074                read_xpm_pixel(&mut self.r, &self.info, &palette, chunk.try_into().unwrap())
1075                    .apply_after(&mut self.r.inner.error)?;
1076            }
1077
1078            if i >= (self.info.height - 1) as usize {
1079                // Last row,
1080            } else {
1081                read_xpm_row_transition(&mut self.r).apply_after(&mut self.r.inner.error)?;
1082            }
1083        }
1084
1085        read_xpm_trailing(&mut self.r).apply_after(&mut self.r.inner.error)?;
1086
1087        Ok(())
1088    }
1089    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
1090        (*self).read_image(buf)
1091    }
1092
1093    fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
1094        limits.check_support(&LimitSupport::default())?;
1095        let (width, height) = self.dimensions();
1096        limits.check_dimensions(width, height)?;
1097
1098        let max_pixels = u64::from(self.info.width) * u64::from(self.info.height);
1099        let max_image_bytes =
1100            max_pixels
1101                .checked_mul(8)
1102                .ok_or(ImageError::Limits(LimitError::from_kind(
1103                    LimitErrorKind::DimensionError,
1104                )))?;
1105
1106        let max_table_bytes = (self.info.ncolors as u64) * (size_of::<XpmColorCodeEntry>() as u64);
1107        let max_bytes = max_image_bytes
1108            .checked_add(max_table_bytes)
1109            .ok_or(ImageError::Limits(LimitError::from_kind(
1110                LimitErrorKind::InsufficientMemory,
1111            )))?;
1112
1113        let max_alloc = limits.max_alloc.unwrap_or(u64::MAX);
1114        if max_alloc < max_bytes {
1115            return Err(ImageError::Limits(LimitError::from_kind(
1116                LimitErrorKind::InsufficientMemory,
1117            )));
1118        }
1119        Ok(())
1120    }
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125    use super::*;
1126
1127    #[test]
1128    fn image_missing_body() {
1129        let data = b"/* XPM */
1130static char *test[] = {
1131\"20 5 10 1\",
1132};
1133";
1134        let decoder = XpmDecoder::new(&data[..]).unwrap();
1135        let mut image = vec![0; decoder.total_bytes() as usize];
1136        assert!(decoder.read_image(&mut image).is_err());
1137    }
1138
1139    #[test]
1140    fn invalid_color_name() {
1141        let data = b"/* XPM */
1142static char *test[] = {
1143    \"1 1 1 1\",
1144    \"  c Antique White1\",
1145    \" \",
1146};";
1147        let decoder = XpmDecoder::new(&data[..]).unwrap();
1148        let mut image = vec![0; decoder.total_bytes() as usize];
1149        assert!(decoder.read_image(&mut image).is_err());
1150    }
1151
1152    #[test]
1153    fn trailing_semicolon_required() {
1154        let data = b"/* XPM */
1155        static char *test[] = {
1156        \"1 1 1 1\",
1157        \"  c none\",
1158        \" \",
1159    };";
1160        let decoder = XpmDecoder::new(&data[..data.len() - 1]).unwrap();
1161        let mut image = vec![0; decoder.total_bytes() as usize];
1162        assert!(decoder.read_image(&mut image).is_err());
1163
1164        let decoder = XpmDecoder::new(&data[..]).unwrap();
1165        let mut image = vec![0; decoder.total_bytes() as usize];
1166        assert!(decoder.read_image(&mut image).is_ok());
1167    }
1168}