1use std::fmt;
14use std::str::FromStr;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct GlibcVersion {
21 pub major: u32,
23 pub minor: u32,
25 pub patch: u32,
27}
28
29impl GlibcVersion {
30 #[must_use]
37 pub fn parse_version_tag(name: &[u8]) -> Option<Self> {
38 let rest = name.strip_prefix(b"GLIBC_")?;
39 let mut numbers = rest.split(|byte| *byte == b'.');
40 let major = parse_digits(numbers.next()?)?;
41 let minor = parse_digits(numbers.next()?)?;
42 let patch = numbers.next().map_or(Some(0), parse_digits)?;
43 numbers.next().is_none().then_some(Self {
44 major,
45 minor,
46 patch,
47 })
48 }
49}
50
51fn parse_digits(bytes: &[u8]) -> Option<u32> {
52 if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
53 return None;
54 }
55 let text = std::str::from_utf8(bytes).ok()?;
56 text.parse().ok()
57}
58
59impl fmt::Display for GlibcVersion {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 if self.patch == 0 {
65 write!(f, "{}.{}", self.major, self.minor)
66 } else {
67 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
68 }
69 }
70}
71
72impl FromStr for GlibcVersion {
76 type Err = GlibcVersionParseError;
77 fn from_str(text: &str) -> Result<Self, Self::Err> {
78 let mut numbers = text.split('.');
79 let parse = |part: Option<&str>| {
80 part.and_then(|part| part.parse::<u32>().ok())
81 .ok_or(GlibcVersionParseError)
82 };
83 let version = Self {
84 major: parse(numbers.next())?,
85 minor: parse(numbers.next())?,
86 patch: numbers.next().map_or(Ok(0), |part| parse(Some(part)))?,
87 };
88 if numbers.next().is_some() {
89 return Err(GlibcVersionParseError);
90 }
91 Ok(version)
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct GlibcVersionParseError;
98
99impl fmt::Display for GlibcVersionParseError {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str("expected glibc version `major.minor[.patch]`")
102 }
103}
104
105impl std::error::Error for GlibcVersionParseError {}
106
107pub const GLIBC_BASELINE: GlibcVersion = GlibcVersion {
115 major: 2,
116 minor: 28,
117 patch: 0,
118};
119
120impl serde::Serialize for GlibcVersion {
123 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
124 serializer.collect_str(self)
125 }
126}
127
128impl<'de> serde::Deserialize<'de> for GlibcVersion {
129 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
130 let text = String::deserialize(deserializer)?;
131 Self::from_str(&text).map_err(serde::de::Error::custom)
132 }
133}
134
135#[cfg(not(target_arch = "wasm32"))]
139mod measure {
140 use object::read::elf::{ElfFile, FileHeader};
141 use std::io::Read as _;
142
143 use super::GlibcVersion;
144 use crate::error::Result;
145 use crate::stow_error;
146
147 pub fn min_glibc_of_elf_bytes(bytes: &[u8]) -> Result<Option<GlibcVersion>> {
159 let Ok(file) = object::File::parse(bytes) else {
160 return Ok(None);
161 };
162 match file {
163 object::File::Elf32(elf) => needed_glibc(&elf),
164 object::File::Elf64(elf) => needed_glibc(&elf),
165 _ => Ok(None),
166 }
167 }
168
169 fn needed_glibc<Elf: FileHeader>(elf: &ElfFile<'_, Elf>) -> Result<Option<GlibcVersion>> {
174 let endian = elf.endian();
175 let data = elf.data();
176 let sections = elf.elf_section_table();
177 let Some((verneeds, strings_index)) = sections
178 .gnu_verneed(endian, data)
179 .map_err(|error| stow_error!("read ELF verneed section: {error}"))?
180 else {
181 return Ok(None);
182 };
183 let strings = sections
184 .strings(endian, data, strings_index)
185 .map_err(|error| stow_error!("read ELF verneed string table: {error}"))?;
186 let mut floor = None;
187 for verneed in verneeds {
188 let (_verneed, aux_iterator) =
189 verneed.map_err(|error| stow_error!("read ELF verneed entry: {error}"))?;
190 for aux in aux_iterator {
191 let aux = aux.map_err(|error| stow_error!("read ELF vernaux entry: {error}"))?;
192 let name = aux
193 .name(endian, strings)
194 .map_err(|error| stow_error!("read ELF vernaux name: {error}"))?;
195 if let Some(version) = GlibcVersion::parse_version_tag(name) {
196 floor = floor.max(Some(version));
197 }
198 }
199 }
200 Ok(floor)
201 }
202
203 pub fn min_glibc_of_bundle(bundle_bytes: &[u8]) -> Result<Option<GlibcVersion>> {
216 let mut archive = tar::Archive::new(bundle_bytes);
217 let mut floor = None;
218 for entry in archive
219 .entries()
220 .map_err(|error| stow_error!("read bundle tar: {error}"))?
221 {
222 let mut entry = entry.map_err(|error| stow_error!("read bundle entry: {error}"))?;
223 let path = entry.path_bytes().into_owned();
225 if !path.starts_with(b"files/") {
226 continue;
227 }
228 let mut compressed = Vec::new();
229 entry.read_to_end(&mut compressed).map_err(|error| {
230 stow_error!(
231 "read bundle member {}: {error}",
232 String::from_utf8_lossy(&path)
233 )
234 })?;
235 let bytes =
236 zstd::stream::decode_all(std::io::Cursor::new(&compressed)).unwrap_or(compressed);
237 floor = floor.max(min_glibc_of_elf_bytes(&bytes)?);
238 }
239 Ok(floor)
240 }
241}
242
243#[cfg(not(target_arch = "wasm32"))]
244pub use measure::{min_glibc_of_bundle, min_glibc_of_elf_bytes};
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn parses_version_tags() {
252 assert_eq!(
253 GlibcVersion::parse_version_tag(b"GLIBC_2.28"),
254 Some(GlibcVersion {
255 major: 2,
256 minor: 28,
257 patch: 0,
258 })
259 );
260 assert_eq!(
261 GlibcVersion::parse_version_tag(b"GLIBC_2.2.5"),
262 Some(GlibcVersion {
263 major: 2,
264 minor: 2,
265 patch: 5,
266 })
267 );
268 assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_PRIVATE"), None);
269 assert_eq!(GlibcVersion::parse_version_tag(b"GCC_3.0"), None);
270 assert_eq!(GlibcVersion::parse_version_tag(b"GLIBCXX_3.4.29"), None);
271 assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_2"), None);
272 assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_2.28.1.9"), None);
273 }
274
275 #[test]
276 fn orders_versions() {
277 let mut versions = [
278 GlibcVersion {
279 major: 2,
280 minor: 28,
281 patch: 0,
282 },
283 GlibcVersion {
284 major: 2,
285 minor: 2,
286 patch: 5,
287 },
288 GlibcVersion {
289 major: 2,
290 minor: 2,
291 patch: 4,
292 },
293 GlibcVersion {
294 major: 2,
295 minor: 35,
296 patch: 0,
297 },
298 ];
299 versions.sort();
300 assert_eq!(
301 versions,
302 [
303 GlibcVersion {
304 major: 2,
305 minor: 2,
306 patch: 4
307 },
308 GlibcVersion {
309 major: 2,
310 minor: 2,
311 patch: 5
312 },
313 GlibcVersion {
314 major: 2,
315 minor: 28,
316 patch: 0
317 },
318 GlibcVersion {
319 major: 2,
320 minor: 35,
321 patch: 0
322 },
323 ]
324 );
325 }
326
327 #[test]
328 fn display_and_from_str_round_trip() {
329 for text in ["2.28", "2.35", "0.0", "2.2.5"] {
330 let version = GlibcVersion::from_str(text).expect("parse");
331 let rendered = version.to_string();
332 let reparsed = GlibcVersion::from_str(&rendered).expect("reparse");
333 assert_eq!(version, reparsed);
334 }
335 assert!(GlibcVersion::from_str("").is_err());
336 assert!(GlibcVersion::from_str("2").is_err());
337 assert!(GlibcVersion::from_str("2.28.x").is_err());
338 assert!(GlibcVersion::from_str("2.28.0.1").is_err());
339 }
340
341 #[test]
342 fn serde_round_trip() {
343 let version = GlibcVersion {
344 major: 2,
345 minor: 28,
346 patch: 0,
347 };
348 let json = serde_json::to_string(&version).expect("serialize");
349 assert_eq!(json, "\"2.28\"");
350 assert_eq!(
351 serde_json::from_str::<GlibcVersion>(&json).expect("deserialize"),
352 version
353 );
354 }
355
356 #[cfg(not(target_arch = "wasm32"))]
357 #[test]
358 fn measures_a_built_so() {
359 let dir = tempfile::tempdir().expect("tempdir");
363 let source = dir.path().join("probe.c");
364 std::fs::write(
367 &source,
368 "#include <string.h>\nunsigned long stow_probe(const char *s) { return strlen(s); }",
369 )
370 .expect("write");
371 let output = dir.path().join("probe.so");
372 let status = std::process::Command::new("cc")
373 .args(["-shared", "-o"])
374 .arg(&output)
375 .arg(&source)
376 .status()
377 .expect("run cc");
378 assert!(status.success(), "cc failed to build probe .so");
379 let bytes = std::fs::read(&output).expect("read probe .so");
380 let floor = min_glibc_of_elf_bytes(&bytes).expect("measure");
381 if cfg!(all(target_os = "linux", target_env = "gnu")) {
384 assert!(floor.is_some(), "probe .so should need some glibc");
385 }
386 }
387
388 #[cfg(not(target_arch = "wasm32"))]
389 #[test]
390 fn non_elf_bytes_have_no_floor() {
391 assert_eq!(
392 min_glibc_of_elf_bytes(b"not an elf file").expect("measure"),
393 None
394 );
395 assert_eq!(min_glibc_of_elf_bytes(&[]).expect("measure"), None);
396 }
397}