Skip to main content

gpui_rhai/
font.rs

1use std::hash::{Hash, Hasher};
2
3use thiserror::Error;
4
5const MAX_FONT_BYTES: usize = 16 * 1024 * 1024;
6
7#[derive(Clone, Debug)]
8pub struct FontSource {
9    label: String,
10    bytes: Vec<u8>,
11    fingerprint: u64,
12}
13
14impl FontSource {
15    /// Validate one in-memory OpenType/TrueType collection supplied by the Host.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`FontError`] for an unsafe label, unsupported header, empty
20    /// payload, or a payload larger than 16 MiB.
21    pub fn new(label: impl Into<String>, bytes: Vec<u8>) -> Result<Self, FontError> {
22        let label = label.into();
23        if label.trim().is_empty() || label.len() > 256 {
24            return Err(FontError::InvalidLabel(label));
25        }
26        if bytes.is_empty() || bytes.len() > MAX_FONT_BYTES {
27            return Err(FontError::InvalidSize {
28                label,
29                size: bytes.len(),
30            });
31        }
32        let header = bytes.get(..4).ok_or_else(|| FontError::UnsupportedFormat {
33            label: label.clone(),
34        })?;
35        if header != [0x00, 0x01, 0x00, 0x00]
36            && header != b"OTTO"
37            && header != b"true"
38            && header != b"ttcf"
39        {
40            return Err(FontError::UnsupportedFormat { label });
41        }
42        let mut hasher = std::collections::hash_map::DefaultHasher::new();
43        bytes.hash(&mut hasher);
44        Ok(Self {
45            label,
46            bytes,
47            fingerprint: hasher.finish(),
48        })
49    }
50
51    #[must_use]
52    pub fn label(&self) -> &str {
53        &self.label
54    }
55
56    #[must_use]
57    pub fn bytes(&self) -> &[u8] {
58        &self.bytes
59    }
60
61    #[must_use]
62    pub const fn fingerprint(&self) -> u64 {
63        self.fingerprint
64    }
65
66    pub(crate) fn into_bytes(self) -> Vec<u8> {
67        self.bytes
68    }
69}
70
71/// Validate a bounded font source set and reject duplicate payloads.
72///
73/// # Errors
74///
75/// Returns [`FontError`] above 64 sources or when the same bytes are declared
76/// more than once.
77pub fn validate_font_sources(sources: &[FontSource]) -> Result<(), FontError> {
78    if sources.len() > 64 {
79        return Err(FontError::TooMany(sources.len()));
80    }
81    let mut fingerprints = std::collections::BTreeSet::new();
82    for source in sources {
83        if !fingerprints.insert(source.fingerprint) {
84            return Err(FontError::Duplicate(source.label.clone()));
85        }
86    }
87    Ok(())
88}
89
90#[derive(Clone, Debug, Error, Eq, PartialEq)]
91pub enum FontError {
92    #[error("font label `{0}` must be non-empty and no longer than 256 bytes")]
93    InvalidLabel(String),
94    #[error("font `{label}` must contain 1..=16777216 bytes, got {size}")]
95    InvalidSize { label: String, size: usize },
96    #[error("font `{label}` is not a supported TrueType/OpenType font or collection")]
97    UnsupportedFormat { label: String },
98    #[error("at most 64 font sources may be declared, got {0}")]
99    TooMany(usize),
100    #[error("font `{0}` duplicates an already declared payload")]
101    Duplicate(String),
102    #[error("GPUI failed to load declared fonts: {0}")]
103    Load(String),
104    #[error("failed to read font `{path}`: {message}")]
105    Io { path: String, message: String },
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn font_sources_validate_format_size_and_duplicates() {
114        let font = FontSource::new("Art", [b"OTTO".as_slice(), &[0, 1, 2]].concat()).unwrap();
115        validate_font_sources(std::slice::from_ref(&font)).unwrap();
116        assert!(matches!(
117            validate_font_sources(&[font.clone(), font]),
118            Err(FontError::Duplicate(_))
119        ));
120        assert!(matches!(
121            FontSource::new("bad", b"woff".to_vec()),
122            Err(FontError::UnsupportedFormat { .. })
123        ));
124    }
125}