Skip to main content

libc_abi_tools/
matrix.rs

1//! Cross-release public ABI evidence. Snapshot absence is not historical absence.
2use crate::{
3    glibc::{self, Kind, Snapshot},
4    version::Version,
5};
6use std::{
7    collections::{BTreeMap, BTreeSet},
8    fmt::Write,
9    fs,
10    path::Path,
11};
12
13const MAX_RELEASES: usize = 64;
14const MAX_ROWS: usize = 100_000;
15const MAX_CELLS: usize = 2_000_000;
16const MAX_NAME_BYTES: usize = 16 * 1024 * 1024;
17type Key = (String, String, Version);
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum Expectation {
21    Accept(Kind),
22    Reject,
23    Unknown,
24}
25impl Expectation {
26    pub fn label(self) -> &'static str {
27        match self {
28            Self::Accept(_) => "accept",
29            Self::Reject => "reject",
30            Self::Unknown => "unknown",
31        }
32    }
33}
34
35#[derive(Debug, Default)]
36pub struct Matrix {
37    releases: BTreeSet<Version>,
38    rows: BTreeMap<Key, BTreeMap<Version, Kind>>,
39    name_bytes: usize,
40}
41
42fn cell(value: Option<Kind>) -> String {
43    match value {
44        Some(Kind::Function) => "F".into(),
45        Some(Kind::Object(n)) => format!("D:{n}"),
46        Some(Kind::Tls(n)) => format!("T:{n}"),
47        None => "absent".into(),
48    }
49}
50
51impl Matrix {
52    pub fn load(root: &Path) -> Result<Self, String> {
53        let mut releases = BTreeSet::new();
54        for entry in fs::read_dir(root).map_err(|e| e.to_string())? {
55            let entry = entry.map_err(|e| e.to_string())?;
56            if !entry.file_type().map_err(|e| e.to_string())?.is_dir() {
57                continue;
58            }
59            let name = entry.file_name();
60            let text = name.to_str().ok_or("release directory must be UTF-8")?;
61            if !text.starts_with(|c: char| c.is_ascii_digit()) {
62                continue;
63            }
64            let release: Version = text.parse()?;
65            release.validate_release()?;
66            if text != release.to_string() {
67                return Err(format!("noncanonical release directory {text:?}"));
68            }
69            releases.insert(release);
70            if releases.len() > MAX_RELEASES {
71                return Err("matrix exceeds 64 snapshot releases".into());
72            }
73        }
74        if releases.is_empty() {
75            return Err("matrix has no release snapshots".into());
76        }
77        let mut matrix = Self::default();
78        for release in releases {
79            matrix.insert(Snapshot::load(root, release)?)?;
80        }
81        Ok(matrix)
82    }
83
84    /// Every release in the bundled glibc-abilists data.
85    #[cfg(feature = "bundled-glibc-data")]
86    pub fn bundled() -> Result<Self, String> {
87        let mut matrix = Self::default();
88        for release in glibc::bundled_releases()? {
89            matrix.insert(Snapshot::bundled(release)?)?;
90        }
91        Ok(matrix)
92    }
93
94    pub fn from_snapshots(snapshots: impl IntoIterator<Item = Snapshot>) -> Result<Self, String> {
95        let mut matrix = Self::default();
96        for snapshot in snapshots {
97            matrix.insert(snapshot)?;
98        }
99        if matrix.releases.is_empty() {
100            return Err("matrix has no release snapshots".into());
101        }
102        Ok(matrix)
103    }
104
105    fn insert(&mut self, snapshot: Snapshot) -> Result<(), String> {
106        let release = snapshot.release.validate_release()?;
107        if self.releases.len() >= MAX_RELEASES || !self.releases.insert(release) {
108            return Err("too many or duplicate snapshot releases".into());
109        }
110        if snapshot.libraries.len() != glibc::LIBRARIES.len()
111            || glibc::LIBRARIES
112                .iter()
113                .any(|lib| !snapshot.libraries.contains_key(*lib))
114        {
115            return Err("matrix requires complete snapshots of the eight tracked libraries".into());
116        }
117        for (library, symbols) in snapshot.libraries {
118            for symbol in symbols {
119                if symbol.name.is_empty()
120                    || symbol.name.len() > 1024
121                    || !symbol.name.bytes().all(|b| b.is_ascii_graphic())
122                    || symbol.version > release
123                {
124                    return Err("invalid matrix symbol name or future ABI version".into());
125                }
126                let key = (library.clone(), symbol.name, symbol.version);
127                if !self.rows.contains_key(&key) {
128                    self.name_bytes += key.0.len() + key.1.len();
129                    if self.rows.len() >= MAX_ROWS || self.name_bytes > MAX_NAME_BYTES {
130                        return Err("matrix exceeds row/name limits".into());
131                    }
132                }
133                if self
134                    .rows
135                    .entry(key)
136                    .or_default()
137                    .insert(release, symbol.kind)
138                    .is_some()
139                {
140                    return Err("duplicate library/symbol/version in a snapshot".into());
141                }
142            }
143        }
144        if self.rows.len() * self.releases.len() > MAX_CELLS {
145            return Err("matrix exceeds two million cells".into());
146        }
147        Ok(())
148    }
149
150    /// Exact versioned export membership; not default binding or whole-link resolution.
151    pub fn expect(
152        &self,
153        release: Version,
154        library: &str,
155        name: &str,
156        version: Version,
157    ) -> Expectation {
158        if !self.releases.contains(&release) || !glibc::LIBRARIES.contains(&library) {
159            return Expectation::Unknown;
160        }
161        self.rows
162            .get(&(library.into(), name.into(), version))
163            .and_then(|row| row.get(&release))
164            .copied()
165            .map_or(Expectation::Reject, Expectation::Accept)
166    }
167
168    pub fn to_tsv(&self, symbol: Option<&str>) -> String {
169        let mut text = String::from("target\tlibrary\tsymbol\tversion\tfirst_observed_snapshot");
170        for release in &self.releases {
171            write!(text, "\t{release}").unwrap();
172        }
173        text.push('\n');
174        for ((library, name, version), row) in &self.rows {
175            if symbol.is_some_and(|s| s != name) {
176                continue;
177            }
178            write!(
179                text,
180                "{}\t{library}\t{name}\tGLIBC_{version}\t{}",
181                glibc::TARGET,
182                row.keys().next().unwrap()
183            )
184            .unwrap();
185            for release in &self.releases {
186                write!(text, "\t{}", cell(row.get(release).copied())).unwrap();
187            }
188            text.push('\n');
189        }
190        text
191    }
192
193    pub fn diff_tsv(
194        &self,
195        from: Version,
196        to: Version,
197        symbol: Option<&str>,
198    ) -> Result<String, String> {
199        if !self.releases.contains(&from) || !self.releases.contains(&to) {
200            return Err("diff requires two known snapshots; missing history is unknown".into());
201        }
202        let mut text =
203            String::from("target\tfrom\tto\tchange\tlibrary\tsymbol\tversion\tbefore\tafter\n");
204        for ((library, name, version), row) in &self.rows {
205            if symbol.is_some_and(|s| s != name) {
206                continue;
207            }
208            let old = row.get(&from).copied();
209            let new = row.get(&to).copied();
210            if old == new {
211                continue;
212            }
213            let change = match (old, new) {
214                (None, Some(_)) => "added",
215                (Some(_), None) => "removed",
216                _ => "changed",
217            };
218            writeln!(
219                text,
220                "{}\t{from}\t{to}\t{change}\t{library}\t{name}\tGLIBC_{version}\t{}\t{}",
221                glibc::TARGET,
222                cell(old),
223                cell(new)
224            )
225            .unwrap();
226        }
227        Ok(text)
228    }
229
230    pub fn changes_tsv(&self) -> String {
231        let releases: Vec<_> = self.releases.iter().copied().collect();
232        let mut text =
233            String::from("target\tfrom\tto\tchange\tlibrary\tsymbol\tversion\tbefore\tafter\n");
234        for pair in releases.windows(2) {
235            let diff = self.diff_tsv(pair[0], pair[1], None).unwrap();
236            text.push_str(diff.split_once('\n').unwrap().1);
237        }
238        text
239    }
240}