use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GlibcVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl GlibcVersion {
#[must_use]
pub fn parse_version_tag(name: &[u8]) -> Option<Self> {
let rest = name.strip_prefix(b"GLIBC_")?;
let mut numbers = rest.split(|byte| *byte == b'.');
let major = parse_digits(numbers.next()?)?;
let minor = parse_digits(numbers.next()?)?;
let patch = numbers.next().map_or(Some(0), parse_digits)?;
numbers.next().is_none().then_some(Self {
major,
minor,
patch,
})
}
}
fn parse_digits(bytes: &[u8]) -> Option<u32> {
if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
return None;
}
let text = std::str::from_utf8(bytes).ok()?;
text.parse().ok()
}
impl fmt::Display for GlibcVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.patch == 0 {
write!(f, "{}.{}", self.major, self.minor)
} else {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
}
impl FromStr for GlibcVersion {
type Err = GlibcVersionParseError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let mut numbers = text.split('.');
let parse = |part: Option<&str>| {
part.and_then(|part| part.parse::<u32>().ok())
.ok_or(GlibcVersionParseError)
};
let version = Self {
major: parse(numbers.next())?,
minor: parse(numbers.next())?,
patch: numbers.next().map_or(Ok(0), |part| parse(Some(part)))?,
};
if numbers.next().is_some() {
return Err(GlibcVersionParseError);
}
Ok(version)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GlibcVersionParseError;
impl fmt::Display for GlibcVersionParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("expected glibc version `major.minor[.patch]`")
}
}
impl std::error::Error for GlibcVersionParseError {}
pub const GLIBC_BASELINE: GlibcVersion = GlibcVersion {
major: 2,
minor: 28,
patch: 0,
};
impl serde::Serialize for GlibcVersion {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> serde::Deserialize<'de> for GlibcVersion {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
Self::from_str(&text).map_err(serde::de::Error::custom)
}
}
#[cfg(not(target_arch = "wasm32"))]
mod measure {
use object::read::elf::{ElfFile, FileHeader};
use std::io::Read as _;
use super::GlibcVersion;
use crate::error::Result;
use crate::stow_error;
pub fn min_glibc_of_elf_bytes(bytes: &[u8]) -> Result<Option<GlibcVersion>> {
let Ok(file) = object::File::parse(bytes) else {
return Ok(None);
};
match file {
object::File::Elf32(elf) => needed_glibc(&elf),
object::File::Elf64(elf) => needed_glibc(&elf),
_ => Ok(None),
}
}
fn needed_glibc<Elf: FileHeader>(elf: &ElfFile<'_, Elf>) -> Result<Option<GlibcVersion>> {
let endian = elf.endian();
let data = elf.data();
let sections = elf.elf_section_table();
let Some((verneeds, strings_index)) = sections
.gnu_verneed(endian, data)
.map_err(|error| stow_error!("read ELF verneed section: {error}"))?
else {
return Ok(None);
};
let strings = sections
.strings(endian, data, strings_index)
.map_err(|error| stow_error!("read ELF verneed string table: {error}"))?;
let mut floor = None;
for verneed in verneeds {
let (_verneed, aux_iterator) =
verneed.map_err(|error| stow_error!("read ELF verneed entry: {error}"))?;
for aux in aux_iterator {
let aux = aux.map_err(|error| stow_error!("read ELF vernaux entry: {error}"))?;
let name = aux
.name(endian, strings)
.map_err(|error| stow_error!("read ELF vernaux name: {error}"))?;
if let Some(version) = GlibcVersion::parse_version_tag(name) {
floor = floor.max(Some(version));
}
}
}
Ok(floor)
}
pub fn min_glibc_of_bundle(bundle_bytes: &[u8]) -> Result<Option<GlibcVersion>> {
let mut archive = tar::Archive::new(bundle_bytes);
let mut floor = None;
for entry in archive
.entries()
.map_err(|error| stow_error!("read bundle tar: {error}"))?
{
let mut entry = entry.map_err(|error| stow_error!("read bundle entry: {error}"))?;
let path = entry.path_bytes().into_owned();
if !path.starts_with(b"files/") {
continue;
}
let mut compressed = Vec::new();
entry.read_to_end(&mut compressed).map_err(|error| {
stow_error!(
"read bundle member {}: {error}",
String::from_utf8_lossy(&path)
)
})?;
let bytes =
zstd::stream::decode_all(std::io::Cursor::new(&compressed)).unwrap_or(compressed);
floor = floor.max(min_glibc_of_elf_bytes(&bytes)?);
}
Ok(floor)
}
}
#[cfg(not(target_arch = "wasm32"))]
pub use measure::{min_glibc_of_bundle, min_glibc_of_elf_bytes};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_version_tags() {
assert_eq!(
GlibcVersion::parse_version_tag(b"GLIBC_2.28"),
Some(GlibcVersion {
major: 2,
minor: 28,
patch: 0,
})
);
assert_eq!(
GlibcVersion::parse_version_tag(b"GLIBC_2.2.5"),
Some(GlibcVersion {
major: 2,
minor: 2,
patch: 5,
})
);
assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_PRIVATE"), None);
assert_eq!(GlibcVersion::parse_version_tag(b"GCC_3.0"), None);
assert_eq!(GlibcVersion::parse_version_tag(b"GLIBCXX_3.4.29"), None);
assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_2"), None);
assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_2.28.1.9"), None);
}
#[test]
fn orders_versions() {
let mut versions = [
GlibcVersion {
major: 2,
minor: 28,
patch: 0,
},
GlibcVersion {
major: 2,
minor: 2,
patch: 5,
},
GlibcVersion {
major: 2,
minor: 2,
patch: 4,
},
GlibcVersion {
major: 2,
minor: 35,
patch: 0,
},
];
versions.sort();
assert_eq!(
versions,
[
GlibcVersion {
major: 2,
minor: 2,
patch: 4
},
GlibcVersion {
major: 2,
minor: 2,
patch: 5
},
GlibcVersion {
major: 2,
minor: 28,
patch: 0
},
GlibcVersion {
major: 2,
minor: 35,
patch: 0
},
]
);
}
#[test]
fn display_and_from_str_round_trip() {
for text in ["2.28", "2.35", "0.0", "2.2.5"] {
let version = GlibcVersion::from_str(text).expect("parse");
let rendered = version.to_string();
let reparsed = GlibcVersion::from_str(&rendered).expect("reparse");
assert_eq!(version, reparsed);
}
assert!(GlibcVersion::from_str("").is_err());
assert!(GlibcVersion::from_str("2").is_err());
assert!(GlibcVersion::from_str("2.28.x").is_err());
assert!(GlibcVersion::from_str("2.28.0.1").is_err());
}
#[test]
fn serde_round_trip() {
let version = GlibcVersion {
major: 2,
minor: 28,
patch: 0,
};
let json = serde_json::to_string(&version).expect("serialize");
assert_eq!(json, "\"2.28\"");
assert_eq!(
serde_json::from_str::<GlibcVersion>(&json).expect("deserialize"),
version
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn measures_a_built_so() {
let dir = tempfile::tempdir().expect("tempdir");
let source = dir.path().join("probe.c");
std::fs::write(
&source,
"#include <string.h>\nunsigned long stow_probe(const char *s) { return strlen(s); }",
)
.expect("write");
let output = dir.path().join("probe.so");
let status = std::process::Command::new("cc")
.args(["-shared", "-o"])
.arg(&output)
.arg(&source)
.status()
.expect("run cc");
assert!(status.success(), "cc failed to build probe .so");
let bytes = std::fs::read(&output).expect("read probe .so");
let floor = min_glibc_of_elf_bytes(&bytes).expect("measure");
if cfg!(all(target_os = "linux", target_env = "gnu")) {
assert!(floor.is_some(), "probe .so should need some glibc");
}
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn non_elf_bytes_have_no_floor() {
assert_eq!(
min_glibc_of_elf_bytes(b"not an elf file").expect("measure"),
None
);
assert_eq!(min_glibc_of_elf_bytes(&[]).expect("measure"), None);
}
}