1use 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 #[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 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}