document-graphics 0.1.5

Render graphics from untrusted documents to RGBA, with resource limits and isolated workers.
Documentation
use super::*;

struct Frame {
    attach: Option<NodeId>,
    last: Option<NodeId>,
    branched: bool,
    game: usize,
}

pub(super) fn parse(source: Arc<[u8]>, cancel: &AtomicBool) -> Result<Collection, Error> {
    check(cancel)?;
    if source.len() > MAX_SOURCE {
        return Err(Error::Limit("SGF source exceeds 8 MiB"));
    }
    let mut doc = Collection {
        source,
        nodes: Vec::new(),
        properties: Vec::new(),
        values: Vec::new(),
        games: Vec::new(),
    };
    let mut stack: Vec<Frame> = Vec::new();
    let mut tails: Vec<Option<NodeId>> = Vec::new();
    let mut i = if doc.source.starts_with(&[0xef, 0xbb, 0xbf]) {
        3
    } else {
        0
    };
    while i < doc.source.len() {
        check(cancel)?;
        match doc.source[i] {
            c if c.is_ascii_whitespace() => {
                i += 1;
            }
            b'(' => {
                if stack.len() >= 256 {
                    return Err(Error::Limit("SGF variation nesting exceeds 256"));
                }
                let (attach, game) = if let Some(frame) = stack.last_mut() {
                    let parent = frame.last.ok_or_else(|| invalid(i, "Empty SGF sequence"))?;
                    frame.branched = true;
                    (Some(parent), frame.game)
                } else {
                    if doc.games.len() >= 1024 {
                        return Err(Error::Limit("SGF collection exceeds 1024 games"));
                    }
                    let game = doc.games.len();
                    doc.games.push(Game {
                        root: doc.nodes.len(),
                        width: 19,
                        height: 19,
                        encoding: Encoding::Latin1,
                    });
                    (None, game)
                };
                stack.push(Frame {
                    attach,
                    game,
                    last: None,
                    branched: false,
                });
                i += 1;
            }
            b')' => {
                let frame = stack
                    .pop()
                    .ok_or_else(|| invalid(i, "Unexpected closing parenthesis"))?;
                if frame.last.is_none() {
                    return Err(invalid(i, "Empty SGF game tree"));
                }
                i += 1;
            }
            b';' => {
                let frame = stack
                    .last_mut()
                    .ok_or_else(|| invalid(i, "Node outside an SGF game tree"))?;
                if frame.branched {
                    return Err(invalid(i, "Node follows a variation outside its sequence"));
                }
                if doc.nodes.len() >= MAX_NODES {
                    return Err(Error::Limit("SGF exceeds 100,000 nodes"));
                }
                let id = doc.nodes.len();
                let parent = frame.last.or(frame.attach);
                doc.nodes.push(Node {
                    parent,
                    first_child: None,
                    next_sibling: None,
                    source: i..i + 1,
                    game: frame.game,
                    properties: doc.properties.len()..doc.properties.len(),
                });
                tails.push(None);
                if let Some(parent) = parent {
                    if let Some(tail) = tails[parent] {
                        doc.nodes[tail].next_sibling = Some(id);
                    } else {
                        doc.nodes[parent].first_child = Some(id);
                    }
                    tails[parent] = Some(id);
                }
                frame.last = Some(id);
                i += 1;
            }
            b'A'..=b'Z' => {
                let id = stack
                    .last()
                    .filter(|f| !f.branched)
                    .and_then(|f| f.last)
                    .ok_or_else(|| invalid(i, "Property outside a node"))?;
                if doc.properties.len() >= 300_000 || doc.nodes[id].properties.len() >= 128 {
                    return Err(Error::Limit("Too many SGF properties"));
                }
                let start = i;
                while i < doc.source.len() && doc.source[i].is_ascii_uppercase() {
                    i += 1;
                }
                if i - start > 32 {
                    return Err(Error::Limit("SGF property name exceeds 32 bytes"));
                }
                let name = start..i;
                if doc.properties[doc.nodes[id].properties.clone()]
                    .iter()
                    .any(|p| doc.source[p.name.clone()] == doc.source[name.clone()])
                {
                    return Err(invalid(start, "Duplicate SGF property"));
                }
                skip(&doc.source, &mut i);
                let first = doc.values.len();
                while i < doc.source.len() && doc.source[i] == b'[' {
                    if doc.values.len() >= 500_000 {
                        return Err(Error::Limit("Too many SGF values"));
                    }
                    i += 1;
                    let start = i;
                    loop {
                        if i >= doc.source.len() {
                            return Err(invalid(start, "Unterminated SGF value"));
                        }
                        if (i - start).is_multiple_of(4096) {
                            check(cancel)?;
                        }
                        if i - start > MAX_VALUE {
                            return Err(Error::Limit("SGF property value exceeds 128 KiB"));
                        }
                        match doc.source[i] {
                            b']' => break,
                            b'\\' => {
                                i += 1;
                                if i >= doc.source.len() {
                                    return Err(invalid(start, "Unterminated SGF escape"));
                                }
                            }
                            _ => {}
                        }
                        i += 1;
                    }
                    doc.values.push(start..i);
                    i += 1;
                    skip(&doc.source, &mut i);
                }
                if first == doc.values.len() {
                    return Err(invalid(start, "SGF property has no value"));
                }
                doc.properties.push(Property {
                    name,
                    values: first..doc.values.len(),
                });
                doc.nodes[id].properties.end = doc.properties.len();
                doc.nodes[id].source.end = i;
            }
            _ => return Err(invalid(i, "Unexpected SGF input")),
        }
    }
    if !stack.is_empty() {
        return Err(invalid(i, "Unclosed SGF game tree"));
    }
    if doc.nodes.is_empty() {
        return Err(invalid(i, "No SGF game found"));
    }
    for g in 0..doc.games.len() {
        let root = doc.games[g].root;
        let start = doc.nodes[root].source.start;
        let end = doc
            .games
            .get(g + 1)
            .map_or(doc.source.len(), |g| doc.nodes[g.root].source.start);
        let utf8 = std::str::from_utf8(&doc.source[start..end]).is_ok();
        if utf8 {
            doc.games[g].encoding = Encoding::Utf8;
        }
        if doc.text(root, "GM").is_some_and(|s| s.trim() != "1") {
            return Err(Error::Unsupported("Only Go records (GM[1]) are supported"));
        }
        if let Some(ff) = doc.text(root, "FF")
            && !matches!(ff.trim(), "1" | "2" | "3" | "4")
        {
            return Err(Error::Unsupported("Unsupported SGF file-format version"));
        }
        if let Some(ca) = doc.text(root, "CA") {
            doc.games[g].encoding = match ca.trim().to_ascii_lowercase().as_str() {
                "utf-8" | "utf8" if utf8 => Encoding::Utf8,
                "utf-8" | "utf8" => {
                    return Err(invalid(
                        doc.nodes[root].source.start,
                        "Invalid UTF-8 in SGF",
                    ));
                }
                "iso-8859-1" | "iso8859-1" | "latin1" => Encoding::Latin1,
                _ => {
                    return Err(Error::Unsupported(
                        "SGF character encoding is not UTF-8 or ISO-8859-1",
                    ));
                }
            };
        }
        if let Some(size) = doc.text(root, "SZ") {
            let (w, h) = size.split_once(':').unwrap_or((&size, &size));
            let size = |s: &str| s.trim().parse::<u8>().ok().filter(|n| (1..=52).contains(n));
            let (Some(w), Some(h)) = (size(w), size(h)) else {
                return Err(Error::Unsupported(
                    "SGF board sizes must be between 1 and 52",
                ));
            };
            doc.games[g].width = w;
            doc.games[g].height = h;
        }
    }
    doc.nodes.shrink_to_fit();
    doc.properties.shrink_to_fit();
    doc.values.shrink_to_fit();
    doc.games.shrink_to_fit();
    check(cancel)?;
    Ok(doc)
}
fn skip(source: &[u8], i: &mut usize) {
    while *i < source.len() && source[*i].is_ascii_whitespace() {
        *i += 1;
    }
}