use std::{
env,
fs::File,
io::Read,
path::PathBuf,
process::{Command, Stdio},
};
use toml::Value;
use crate::ModuleInfoResult;
pub(crate) fn bytes_to_linker_directives(bytes: &[u8]) -> String {
bytes
.iter()
.map(|b| format!(" BYTE(0x{b:02X});"))
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn align_len(len: u32, align: usize) -> u32 {
debug_assert!(align.is_power_of_two(), "align must be a power of two");
let align_u32 = u32::try_from(align).unwrap_or(u32::MAX);
let mask = align_u32.saturating_sub(1);
match len.checked_add(mask) {
Some(sum) => sum & !mask,
None => u32::MAX,
}
}
#[must_use = "distro info is the sole output of this function; discarding it yields no useful side effects"]
pub fn get_distro_info() -> ModuleInfoResult<(String, String)> {
const MAX_FILE_SIZE: usize = 10 * 1024;
let os_release = std::fs::File::open("/etc/os-release");
if let Err(ref e) = os_release {
debug!("get_distro_info: /etc/os-release unavailable: {}", e);
}
if let Ok(file) = os_release {
let mut limited = file.take(MAX_FILE_SIZE as u64 + 1);
let mut bytes = Vec::new();
let bytes_read = limited
.read_to_end(&mut bytes)
.map_err(crate::ModuleInfoError::IoError)?;
if bytes_read > MAX_FILE_SIZE {
return Err(crate::ModuleInfoError::Other(
format!("os-release file too large: exceeds {MAX_FILE_SIZE} bytes").into(),
));
}
let content = String::from_utf8_lossy(&bytes).into_owned();
let mut name = String::new();
let mut version = String::new();
for line in content.lines() {
if line.starts_with("ID=") {
name = line.trim_start_matches("ID=").trim_matches('"').to_string();
} else if line.starts_with("VERSION_ID=") {
version = line
.trim_start_matches("VERSION_ID=")
.trim_matches('"')
.to_string();
}
}
if !name.is_empty() {
return Ok((name, version));
}
}
Ok(("Linux".to_string(), "Unknown".to_string()))
}
#[must_use = "git info is the sole output of this function; ignoring it means the call did no useful work"]
pub fn get_git_info() -> ModuleInfoResult<(String, String, String)> {
let project_path = get_project_path();
let run_git = |args: &[&str]| -> Option<String> {
match Command::new("git")
.current_dir(&project_path)
.args(args)
.stdin(Stdio::null())
.output()
{
Ok(output) if output.status.success() => {
match std::str::from_utf8(&output.stdout) {
Ok(s) => Some(s.trim().to_string()),
Err(e) => {
debug!(
"git {:?} returned non-UTF-8 stdout ({e}); treating as 'unknown'",
args
);
None
}
}
}
Ok(output) => {
debug!(
"git {:?} exited with {}: {}",
args,
output.status,
String::from_utf8_lossy(&output.stderr).trim()
);
None
}
Err(e) => {
debug!("git {:?} failed to spawn: {}", args, e);
None
}
}
};
let branch =
run_git(&["rev-parse", "--abbrev-ref", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
let hash = run_git(&["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
let repo_name = run_git(&["remote", "get-url", "origin"])
.and_then(|url| parse_repo_name_from_url(&url))
.or_else(|| {
project_path
.file_name()
.map(|name| name.to_string_lossy().to_string())
})
.unwrap_or_else(|| "unknown".to_string());
Ok((branch, hash, repo_name))
}
fn parse_repo_name_from_url(url: &str) -> Option<String> {
let url = url.trim();
let url = url.split_once(['?', '#']).map_or(url, |(before, _)| before);
let url = url.strip_suffix(".git").unwrap_or(url);
url.rsplit(['/', ':'])
.find(|s| !s.is_empty())
.map(str::to_string)
}
pub fn get_project_path() -> PathBuf {
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
return PathBuf::from(manifest_dir);
}
let mut path = match env::current_dir() {
Ok(p) => p,
Err(_) => PathBuf::from("."),
};
while !path.join("Cargo.toml").exists() {
if !path.pop() {
return match env::current_dir() {
Ok(p) => p,
Err(_) => PathBuf::from("."),
};
}
}
path
}
#[must_use = "the parsed Cargo.toml is the sole output of this function; discarding it wastes the I/O and parse work"]
pub fn get_cargo_toml_content() -> ModuleInfoResult<Value> {
let project_path = get_project_path();
let cargo_path = project_path.join("Cargo.toml");
let mut file = File::open(cargo_path)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let cargo_toml: Value = toml::from_str(&content)?;
Ok(cargo_toml)
}
#[cfg(test)]
mod tests {
use super::parse_repo_name_from_url;
#[test]
fn parses_common_remote_shapes() {
let cases = [
("https://github.com/user/repo.git", Some("repo".to_string())),
("https://github.com/user/repo", Some("repo".to_string())),
("git@github.com:user/repo.git", Some("repo".to_string())),
(
"https://dev.azure.com/org/project/_git/repo",
Some("repo".to_string()),
),
("https://github.com/user/repo/", Some("repo".to_string())),
("", None),
(" ", None),
("/", None),
];
for (input, expected) in cases {
assert_eq!(
parse_repo_name_from_url(input),
expected,
"unexpected result for input {input:?}"
);
}
}
}