1use crate::version::Version;
6use std::{
7 collections::BTreeMap,
8 fmt::Write as _,
9 fs,
10 io::Read,
11 path::{Path, PathBuf},
12 process::Command,
13};
14
15pub const TARGET: &str = "x86_64-unknown-linux-gnu";
17pub const LIBRARIES: [&str; 8] = ["m", "c", "ld", "resolv", "pthread", "dl", "rt", "util"];
19const ABI_PATH: &str = "sysdeps/unix/sysv/linux/x86_64/64";
20const MAX_FILE_BYTES: u64 = 10 * 1024 * 1024;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
24pub enum Kind {
25 Function,
27 Object(u16),
29 Tls(u16),
31}
32
33#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
35pub struct Symbol {
36 pub name: String,
38 pub version: Version,
40 pub kind: Kind,
42}
43
44pub fn parse_abilist(text: &str) -> Result<Vec<Symbol>, String> {
53 let mut group: Option<&str> = None;
54 let mut records = BTreeMap::new();
55 for (index, line) in text.lines().enumerate() {
56 let fail = |message: &str| format!("line {}: {message}", index + 1);
57 let fields: Vec<_> = line.split_ascii_whitespace().collect();
58 if fields.is_empty() {
59 continue;
60 }
61 let indented = line.starts_with([' ', '\t']);
62 if !indented && fields.len() == 1 {
63 validate_namespace(fields[0]).map_err(|e| fail(&e))?;
64 group = Some(fields[0]);
65 continue;
66 }
67 let (namespace, values) = if indented {
68 (
69 group.ok_or_else(|| fail("symbol before a version group"))?,
70 &fields[..],
71 )
72 } else {
73 group = None;
75 (fields[0], &fields[1..])
76 };
77 validate_namespace(namespace).map_err(|e| fail(&e))?;
78 if values.len() < 2 {
79 return Err(fail("expected a symbol name and kind"));
80 }
81 let name = values[0];
82 if !name.bytes().all(|b| b.is_ascii_graphic()) {
83 return Err(fail(
84 "symbol names must contain printable ASCII without spaces",
85 ));
86 }
87 let size = || -> Result<u16, String> {
88 if values.len() != 3 {
89 return Err(fail("data/TLS symbols require exactly one size"));
90 }
91 let raw = values[2];
92 let (digits, radix) = raw.strip_prefix("0x").map_or((raw, 10), |s| (s, 16));
93 if digits.is_empty()
94 || !digits.bytes().all(|b| {
95 if radix == 16 {
96 b.is_ascii_hexdigit()
97 } else {
98 b.is_ascii_digit()
99 }
100 })
101 {
102 return Err(fail("invalid symbol size"));
103 }
104 u16::from_str_radix(digits, radix).map_err(|_| fail("symbol size exceeds u16"))
105 };
106 let kind = match values[1] {
107 "F" if values.len() == 2 => Kind::Function,
108 "D" => Kind::Object(size()?),
109 "T" => Kind::Tls(size()?),
110 "A" if values.len() == 2 => continue,
111 _ => return Err(fail("unsupported symbol kind or extra fields")),
112 };
113 if namespace.starts_with("GCC_") {
114 continue;
115 }
116 let version: Version = namespace[6..].parse().map_err(|e: String| fail(&e))?;
117 let key = (name.to_owned(), version);
118 if let Some(previous) = records.insert(key, kind) {
119 if previous != kind {
120 return Err(fail("conflicting duplicate symbol/version"));
121 }
122 }
123 }
124 Ok(records
125 .into_iter()
126 .map(|((name, version), kind)| Symbol {
127 name,
128 version,
129 kind,
130 })
131 .collect())
132}
133
134fn validate_namespace(text: &str) -> Result<(), String> {
135 let numeric = text
136 .strip_prefix("GLIBC_")
137 .or_else(|| text.strip_prefix("GCC_"))
138 .ok_or_else(|| format!("unsupported version namespace {text:?}"))?;
139 numeric.parse::<Version>().map(|_| ())
140}
141
142#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct Snapshot {
145 pub release: Version,
147 pub libraries: BTreeMap<String, Vec<Symbol>>,
150}
151
152impl Snapshot {
153 pub fn load(root: &Path, release: Version) -> Result<Self, String> {
162 release.validate_release()?;
163 let release_dir = root.join(release.to_string());
164 let (snapshot, _) = load_source(&release_dir, release)?;
165 Ok(snapshot)
166 }
167
168 #[cfg(feature = "bundled-glibc-data")]
176 pub fn bundled(release: Version) -> Result<Self, String> {
177 release.validate_release()?;
178 let (snapshot, _) = load_with(release, |relative| bundled_file(release, relative))?;
179 Ok(snapshot)
180 }
181
182 pub fn to_tsv(&self, symbol: Option<&str>) -> String {
185 let mut out = String::from("release\ttarget\tlibrary\tsymbol\tversion\tkind\tsize\n");
186 for (library, symbols) in &self.libraries {
187 for entry in symbols {
188 if symbol.is_some_and(|name| name != entry.name) {
189 continue;
190 }
191 let (kind, size) = match entry.kind {
192 Kind::Function => ("F", String::from("-")),
193 Kind::Object(size) => ("D", size.to_string()),
194 Kind::Tls(size) => ("T", size.to_string()),
195 };
196 writeln!(
197 out,
198 "{}\t{TARGET}\t{library}\t{}\tGLIBC_{}\t{kind}\t{size}",
199 self.release, entry.name, entry.version
200 )
201 .unwrap();
202 }
203 }
204 out
205 }
206}
207
208fn read_inside(root: &Path, relative: &Path) -> Result<Vec<u8>, String> {
209 let path = root.join(relative);
210 let actual = path
211 .canonicalize()
212 .map_err(|e| format!("{}: {e}", path.display()))?;
213 if !actual.starts_with(root) {
214 return Err(format!(
215 "{} resolves outside the source tree",
216 path.display()
217 ));
218 }
219 if !actual.is_file() {
220 return Err(format!("{} is not a regular file", path.display()));
221 }
222 let mut bytes = Vec::new();
223 fs::File::open(&actual)
224 .map_err(|e| e.to_string())?
225 .take(MAX_FILE_BYTES + 1)
226 .read_to_end(&mut bytes)
227 .map_err(|e| e.to_string())?;
228 if bytes.len() as u64 > MAX_FILE_BYTES {
229 return Err(format!("{} exceeds the 10 MiB input limit", path.display()));
230 }
231 Ok(bytes)
232}
233
234type SourceFiles = Vec<(PathBuf, Vec<u8>)>;
235
236fn load_source(source: &Path, release: Version) -> Result<(Snapshot, SourceFiles), String> {
237 let source = source
238 .canonicalize()
239 .map_err(|e| format!("{}: {e}", source.display()))?;
240 load_with(release, |relative| read_inside(&source, relative))
241}
242
243pub fn abilist_path(release: Version, library: &str) -> PathBuf {
246 let mut path = PathBuf::from(ABI_PATH);
247 if release.minor <= 19 {
248 path.push("nptl");
249 }
250 path.join(if library == "ld" {
251 "ld.abilist".to_owned()
252 } else {
253 format!("lib{library}.abilist")
254 })
255}
256
257fn load_with(
258 release: Version,
259 read: impl Fn(&Path) -> Result<Vec<u8>, String>,
260) -> Result<(Snapshot, SourceFiles), String> {
261 let mut libraries = BTreeMap::new();
262 let mut files = Vec::new();
263 for library in LIBRARIES {
264 let relative = abilist_path(release, library);
265 let bytes = read(&relative)?;
266 let text =
267 std::str::from_utf8(&bytes).map_err(|e| format!("{}: {e}", relative.display()))?;
268 let records = parse_abilist(text).map_err(|e| format!("{}: {e}", relative.display()))?;
269 if records.iter().any(|record| record.version > release) {
270 return Err(format!(
271 "{} contains a symbol newer than release {release}",
272 relative.display()
273 ));
274 }
275 libraries.insert(library.to_owned(), records);
276 files.push((relative, bytes));
277 }
278 Ok((Snapshot { release, libraries }, files))
279}
280
281fn git(source: &Path, args: &[&str]) -> Result<String, String> {
282 let output = Command::new("git")
283 .arg("-C")
284 .arg(source)
285 .args(args)
286 .output()
287 .map_err(|e| format!("cannot run git: {e}"))?;
288 if !output.status.success() {
289 return Err(format!(
290 "git {} failed: {}",
291 args.join(" "),
292 String::from_utf8_lossy(&output.stderr).trim()
293 ));
294 }
295 String::from_utf8(output.stdout)
296 .map(|s| s.trim().to_owned())
297 .map_err(|e| e.to_string())
298}
299
300#[cfg(feature = "bundled-glibc-data")]
302fn bundled_file(release: Version, relative: &Path) -> Result<Vec<u8>, String> {
303 let version = release.to_string();
304 let path = relative.to_string_lossy().replace('\\', "/");
305 glibc_abilists::abilists()
306 .iter()
307 .find(|a| a.release == version && a.path == path)
308 .map(|a| a.text.as_bytes().to_vec())
309 .ok_or_else(|| format!("glibc-abilists has no {path} for {release}"))
310}
311
312#[cfg(feature = "bundled-glibc-data")]
314pub fn bundled_releases() -> Result<Vec<Version>, String> {
315 glibc_abilists::releases()
316 .iter()
317 .map(|r| r.version.parse())
318 .collect()
319}
320
321#[cfg(feature = "bundled-glibc-data")]
325pub fn export_bundled(output: &Path) -> Result<usize, String> {
326 fs::create_dir(output)
327 .map_err(|e| format!("cannot create new directory {}: {e}", output.display()))?;
328 let mut count = 0;
329 for a in glibc_abilists::abilists() {
330 let path = output.join(a.release).join(a.path);
331 if let Some(parent) = path.parent() {
332 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
333 }
334 fs::write(&path, a.text).map_err(|e| format!("{}: {e}", path.display()))?;
335 count += 1;
336 }
337 let mut provenance = String::from(
338 "# ABI lists exported from the glibc-abilists crate; see its data/releases.tsv\n\
339 # and data/files.tsv for each tag's commit, signature, and file hashes.\n\
340 # License: LGPL-2.1-or-later (the GNU C Library's).\n\
341 release\ttag\tcommit\n",
342 );
343 for r in glibc_abilists::releases() {
344 writeln!(provenance, "{}\t{}\t{}", r.version, r.tag, r.commit).unwrap();
345 }
346 fs::write(output.join("SOURCE.tsv"), provenance).map_err(|e| e.to_string())?;
347 fs::write(output.join("COPYING.LIB"), glibc_abilists::LICENSE_TEXT)
349 .map_err(|e| e.to_string())?;
350 Ok(count)
351}
352
353pub fn collect(source: &Path, output: &Path) -> Result<PathBuf, String> {
357 let tag = git(
358 source,
359 &["describe", "--tags", "--exact-match", "--match", "glibc-*"],
360 )?;
361 let release: Version = tag
362 .strip_prefix("glibc-")
363 .ok_or("expected an official glibc release tag")?
364 .parse()?;
365 release.validate_release()?;
366 if tag != format!("glibc-{release}") {
367 return Err("expected an exact glibc-MAJOR.MINOR release tag".into());
368 }
369 if !git(source, &["status", "--porcelain", "--untracked-files=all"])?.is_empty() {
370 return Err("source checkout must be clean before collection".into());
371 }
372 let commit = git(source, &["rev-parse", "HEAD"])?;
373 let (_, mut files) = load_source(source, release)?;
374 let source = source.canonicalize().map_err(|e| e.to_string())?;
375 for name in ["COPYING", "COPYING.LIB"] {
377 files.push((PathBuf::from(name), read_inside(&source, Path::new(name))?));
378 }
379 let destination = output.join(release.to_string());
380 fs::create_dir_all(output).map_err(|e| e.to_string())?;
381 fs::create_dir(&destination)
382 .map_err(|e| format!("cannot create new release {}: {e}", destination.display()))?;
383 for (relative, bytes) in files {
384 let path = destination.join(relative);
385 if let Some(parent) = path.parent() {
386 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
387 }
388 fs::write(&path, bytes).map_err(|e| format!("{}: {e}", path.display()))?;
389 }
390 fs::write(
391 destination.join("SOURCE.txt"),
392 format!("tag={tag}\ncommit={commit}\ntarget={TARGET}\n"),
393 )
394 .map_err(|e| e.to_string())?;
395 Ok(destination)
396}