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/// Whether a link against one release should accept a versioned symbol
20/// ([`Matrix::expect`]).
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum Expectation {
23    /// The release's library exports the symbol at that version, with this kind.
24    Accept(Kind),
25    /// The release is in the matrix and its library does not export it.
26    Reject,
27    /// The release or library is not in the matrix, so nothing is known.
28    Unknown,
29}
30impl Expectation {
31    /// `accept`, `reject` or `unknown`, as `libc-abi glibc expect` prints it.
32    pub fn label(self) -> &'static str {
33        match self {
34            Self::Accept(_) => "accept",
35            Self::Reject => "reject",
36            Self::Unknown => "unknown",
37        }
38    }
39}
40
41/// Every versioned symbol of several glibc releases' snapshots, with the kind
42/// each release defines it as. Built from complete [`Snapshot`]s only.
43#[derive(Debug, Default)]
44pub struct Matrix {
45    releases: BTreeSet<Version>,
46    rows: BTreeMap<Key, BTreeMap<Version, Kind>>,
47    name_bytes: usize,
48}
49
50fn cell(value: Option<Kind>) -> String {
51    match value {
52        Some(Kind::Function) => "F".into(),
53        Some(Kind::Object(n)) => format!("D:{n}"),
54        Some(Kind::Tls(n)) => format!("T:{n}"),
55        None => "absent".into(),
56    }
57}
58
59impl Matrix {
60    /// Loads every release directory under `root` (the data-directory layout of
61    /// [`Snapshot::load`]); entries not named like a release are skipped.
62    ///
63    /// # Errors
64    ///
65    /// Fails if `root` cannot be read, a release directory is not a canonical
66    /// release from 2.17, there are none or more than 64, a snapshot fails to load,
67    /// or the matrix exceeds its size limits.
68    pub fn load(root: &Path) -> Result<Self, String> {
69        let mut releases = BTreeSet::new();
70        for entry in fs::read_dir(root).map_err(|e| e.to_string())? {
71            let entry = entry.map_err(|e| e.to_string())?;
72            if !entry.file_type().map_err(|e| e.to_string())?.is_dir() {
73                continue;
74            }
75            let name = entry.file_name();
76            let text = name.to_str().ok_or("release directory must be UTF-8")?;
77            if !text.starts_with(|c: char| c.is_ascii_digit()) {
78                continue;
79            }
80            let release: Version = text.parse()?;
81            release.validate_release()?;
82            if text != release.to_string() {
83                return Err(format!("noncanonical release directory {text:?}"));
84            }
85            releases.insert(release);
86            if releases.len() > MAX_RELEASES {
87                return Err("matrix exceeds 64 snapshot releases".into());
88            }
89        }
90        if releases.is_empty() {
91            return Err("matrix has no release snapshots".into());
92        }
93        let mut matrix = Self::default();
94        for release in releases {
95            matrix.insert(Snapshot::load(root, release)?)?;
96        }
97        Ok(matrix)
98    }
99
100    /// Every release in the bundled glibc-abilists data.
101    ///
102    /// # Errors
103    ///
104    /// Fails if the bundled data is malformed.
105    #[cfg(feature = "bundled-glibc-data")]
106    pub fn bundled() -> Result<Self, String> {
107        let mut matrix = Self::default();
108        for release in glibc::bundled_releases()? {
109            matrix.insert(Snapshot::bundled(release)?)?;
110        }
111        Ok(matrix)
112    }
113
114    /// Builds a matrix from snapshots already loaded.
115    ///
116    /// # Errors
117    ///
118    /// Fails if there are none, a release repeats, a snapshot lacks one of the
119    /// eight tracked libraries or has an invalid or future-versioned symbol, or the
120    /// matrix exceeds its size limits.
121    pub fn from_snapshots(snapshots: impl IntoIterator<Item = Snapshot>) -> Result<Self, String> {
122        let mut matrix = Self::default();
123        for snapshot in snapshots {
124            matrix.insert(snapshot)?;
125        }
126        if matrix.releases.is_empty() {
127            return Err("matrix has no release snapshots".into());
128        }
129        Ok(matrix)
130    }
131
132    fn insert(&mut self, snapshot: Snapshot) -> Result<(), String> {
133        let release = snapshot.release.validate_release()?;
134        if self.releases.len() >= MAX_RELEASES || !self.releases.insert(release) {
135            return Err("too many or duplicate snapshot releases".into());
136        }
137        if snapshot.libraries.len() != glibc::LIBRARIES.len()
138            || glibc::LIBRARIES
139                .iter()
140                .any(|lib| !snapshot.libraries.contains_key(*lib))
141        {
142            return Err("matrix requires complete snapshots of the eight tracked libraries".into());
143        }
144        for (library, symbols) in snapshot.libraries {
145            for symbol in symbols {
146                if symbol.name.is_empty()
147                    || symbol.name.len() > 1024
148                    || !symbol.name.bytes().all(|b| b.is_ascii_graphic())
149                    || symbol.version > release
150                {
151                    return Err("invalid matrix symbol name or future ABI version".into());
152                }
153                let key = (library.clone(), symbol.name, symbol.version);
154                if !self.rows.contains_key(&key) {
155                    self.name_bytes += key.0.len() + key.1.len();
156                    if self.rows.len() >= MAX_ROWS || self.name_bytes > MAX_NAME_BYTES {
157                        return Err("matrix exceeds row/name limits".into());
158                    }
159                }
160                if self
161                    .rows
162                    .entry(key)
163                    .or_default()
164                    .insert(release, symbol.kind)
165                    .is_some()
166                {
167                    return Err("duplicate library/symbol/version in a snapshot".into());
168                }
169            }
170        }
171        if self.rows.len() * self.releases.len() > MAX_CELLS {
172            return Err("matrix exceeds two million cells".into());
173        }
174        Ok(())
175    }
176
177    /// Exact versioned export membership; not default binding or whole-link resolution.
178    pub fn expect(
179        &self,
180        release: Version,
181        library: &str,
182        name: &str,
183        version: Version,
184    ) -> Expectation {
185        if !self.releases.contains(&release) || !glibc::LIBRARIES.contains(&library) {
186            return Expectation::Unknown;
187        }
188        self.rows
189            .get(&(library.into(), name.into(), version))
190            .and_then(|row| row.get(&release))
191            .copied()
192            .map_or(Expectation::Reject, Expectation::Accept)
193    }
194
195    /// Stable TSV with one row per library, symbol and version: the first
196    /// snapshot that has it, then one column per release (`F`, `D:size`,
197    /// `T:size` or `absent`). With `symbol`, only rows for that name.
198    pub fn to_tsv(&self, symbol: Option<&str>) -> String {
199        let mut text = String::from("target\tlibrary\tsymbol\tversion\tfirst_observed_snapshot");
200        for release in &self.releases {
201            write!(text, "\t{release}").unwrap();
202        }
203        text.push('\n');
204        for ((library, name, version), row) in &self.rows {
205            if symbol.is_some_and(|s| s != name) {
206                continue;
207            }
208            write!(
209                text,
210                "{}\t{library}\t{name}\tGLIBC_{version}\t{}",
211                glibc::TARGET,
212                row.keys().next().unwrap()
213            )
214            .unwrap();
215            for release in &self.releases {
216                write!(text, "\t{}", cell(row.get(release).copied())).unwrap();
217            }
218            text.push('\n');
219        }
220        text
221    }
222
223    /// Stable TSV of the rows that are added, removed or changed between the
224    /// `from` and `to` releases. With `symbol`, only rows for that name.
225    ///
226    /// # Errors
227    ///
228    /// Fails if either release is not in the matrix.
229    pub fn diff_tsv(
230        &self,
231        from: Version,
232        to: Version,
233        symbol: Option<&str>,
234    ) -> Result<String, String> {
235        if !self.releases.contains(&from) || !self.releases.contains(&to) {
236            return Err("diff requires two known snapshots; missing history is unknown".into());
237        }
238        let mut text =
239            String::from("target\tfrom\tto\tchange\tlibrary\tsymbol\tversion\tbefore\tafter\n");
240        for ((library, name, version), row) in &self.rows {
241            if symbol.is_some_and(|s| s != name) {
242                continue;
243            }
244            let old = row.get(&from).copied();
245            let new = row.get(&to).copied();
246            if old == new {
247                continue;
248            }
249            let change = match (old, new) {
250                (None, Some(_)) => "added",
251                (Some(_), None) => "removed",
252                _ => "changed",
253            };
254            writeln!(
255                text,
256                "{}\t{from}\t{to}\t{change}\t{library}\t{name}\tGLIBC_{version}\t{}\t{}",
257                glibc::TARGET,
258                cell(old),
259                cell(new)
260            )
261            .unwrap();
262        }
263        Ok(text)
264    }
265
266    /// [`Matrix::diff_tsv`] between each pair of consecutive releases, under one
267    /// header.
268    pub fn changes_tsv(&self) -> String {
269        let releases: Vec<_> = self.releases.iter().copied().collect();
270        let mut text =
271            String::from("target\tfrom\tto\tchange\tlibrary\tsymbol\tversion\tbefore\tafter\n");
272        for pair in releases.windows(2) {
273            let diff = self.diff_tsv(pair[0], pair[1], None).unwrap();
274            text.push_str(diff.split_once('\n').unwrap().1);
275        }
276        text
277    }
278}