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)]
22pub enum Expectation {
23 Accept(Kind),
25 Reject,
27 Unknown,
29}
30impl Expectation {
31 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#[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 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 #[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 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 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 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 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 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}