mod error;
mod fields;
#[macro_use]
mod macros;
use cfg_if::cfg_if;
pub use error::{ModuleInfoError, ModuleInfoResult};
pub use fields::ModuleInfoField;
cfg_if! {
if #[cfg(target_os = "linux")] {
use std::{env, path::{Path, PathBuf}};
mod constants;
mod metadata;
mod note_section;
mod utils;
pub use metadata::PackageMetadata;
pub(crate) use constants::*;
}
}
cfg_if! {
if #[cfg(all(feature = "embed-module-info", target_os = "linux"))] {
#[link_section = ".note.package"]
#[no_mangle]
#[used]
#[doc(hidden)]
pub static PACKAGE_NOTE_SECTION: [u8; 0] = [];
#[macro_export]
macro_rules! embed {
() => {
#[allow(dead_code)]
const _: () = {
#[used]
static __MODULE_INFO_FORCE_LINK: &'static [u8; 0] =
&$crate::PACKAGE_NOTE_SECTION;
};
};
}
} else if #[cfg(all(feature = "embed-module-info", not(target_os = "linux")))] {
#[macro_export]
macro_rules! embed {
() => {};
}
} else {
#[macro_export]
macro_rules! embed {
() => {};
}
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct EmbedOptions {
pub out_dir: Option<PathBuf>,
pub emit_cargo_link_arg: bool,
}
#[cfg(target_os = "linux")]
impl Default for EmbedOptions {
fn default() -> Self {
Self {
out_dir: None,
emit_cargo_link_arg: true,
}
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct EmbedArtifacts {
pub linker_script_path: PathBuf,
pub note_bin_path: PathBuf,
pub json_path: PathBuf,
pub json: String,
pub linker_script_body: String,
}
#[cfg(target_os = "linux")]
#[allow(non_snake_case)] #[derive(Debug, Clone, Default)]
pub struct Info {
pub binary: String,
pub version: String,
pub moduleVersion: String,
pub maintainer: String,
pub name: String,
pub r#type: String,
pub repo: String,
pub branch: String,
pub hash: String,
pub copyright: String,
pub os: String,
pub osVersion: String,
}
#[cfg(target_os = "linux")]
impl From<Info> for PackageMetadata {
fn from(info: Info) -> Self {
PackageMetadata {
binary: info.binary,
version: info.version,
module_version: info.moduleVersion,
maintainer: info.maintainer,
name: info.name,
module_type: info.r#type,
repo: info.repo,
branch: info.branch,
hash: info.hash,
copyright: info.copyright,
os: info.os,
os_version: info.osVersion,
}
}
}
#[cfg(target_os = "linux")]
#[must_use = "new returns EmbedArtifacts; discarding it hides both the written paths and any I/O errors"]
pub fn new(info: Info) -> ModuleInfoResult<EmbedArtifacts> {
embed_package_metadata(&info.into(), &EmbedOptions::default())
}
#[cfg(target_os = "linux")]
fn validate_module_version(module_version: &str) -> ModuleInfoResult<()> {
let parts: Vec<&str> = module_version.split('.').collect();
if parts.len() != 4 {
return Err(ModuleInfoError::MalformedJson(format!(
"moduleVersion must have exactly 4 dot-separated parts, got {} in {module_version:?}",
parts.len()
)));
}
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
return Err(ModuleInfoError::MalformedJson(format!(
"moduleVersion part {i} is empty in {module_version:?}"
)));
}
if part.parse::<u16>().is_err() {
return Err(ModuleInfoError::MalformedJson(format!(
"moduleVersion part {i} ({part:?}) must be a non-negative integer \
that fits in 16 bits (0..=65535) in {module_version:?}"
)));
}
}
Ok(())
}
#[cfg(target_os = "linux")]
#[must_use = "embed_package_metadata returns EmbedArtifacts; discarding it hides both the written paths and any I/O errors"]
pub fn embed_package_metadata(
md: &PackageMetadata,
opts: &EmbedOptions,
) -> ModuleInfoResult<EmbedArtifacts> {
emit_rerun_if_directives();
let (compact_json, linker_script_body) = metadata::render_note_payloads(md)?;
validate_embedded_json(&compact_json)?;
note!();
note!("-- Module Info --");
emit_metadata_notes(&compact_json);
let out_dir: PathBuf = match &opts.out_dir {
Some(p) => p.clone(),
None => PathBuf::from(env::var("OUT_DIR")?),
};
debug!("OUT_DIR: {}", out_dir.display());
std::fs::create_dir_all(&out_dir)?;
let linker_script_body_path = out_dir.join("linker_script_body.ld.inc");
debug!(
"Writing linker script body to: {}",
linker_script_body_path.display()
);
let linker_script_body_on_disk = format!(
"/* Linker-script fragment. Inlined inside linker_script.ld; not a standalone script. */\n{}",
linker_script_body.trim_start_matches('\n')
);
std::fs::write(
&linker_script_body_path,
linker_script_body_on_disk.as_bytes(),
)?;
let json_path = out_dir.join("module_info.json");
debug!("Writing module info to: {}", json_path.display());
std::fs::write(&json_path, compact_json.as_bytes())?;
let padding = NOTE_ALIGN - (compact_json.len() % NOTE_ALIGN);
let mut descriptor = String::with_capacity(compact_json.len() + padding);
descriptor.push_str(&compact_json);
for _ in 0..padding {
descriptor.push('\0');
}
let note = note_section::NoteSection::new(
N_TYPE,
OWNER,
&descriptor,
&linker_script_body,
NOTE_ALIGN,
)?;
debug!(
"Created note section with {} bytes of data",
note.note_section.len()
);
let note_bin_path = out_dir.join(format!("{}.bin", NOTE_SECTION_NAME.trim_start_matches('.')));
debug!("Saving binary note section to: {}", note_bin_path.display());
note.save_section(¬e_bin_path)?;
debug!("Saving linker script...");
let linker_script_path = note.save_linker_script(&out_dir)?;
debug!("Linker script saved to: {}", linker_script_path.display());
match link_arg_directive(&linker_script_path, opts.emit_cargo_link_arg) {
Some(d) => {
debug!("Adding cargo directive: {}", d);
println!("{d}");
}
None => {
debug!(
"emit_cargo_link_arg=false: caller will pass {} to the final linker",
linker_script_path.display()
);
}
}
Ok(EmbedArtifacts {
linker_script_path,
note_bin_path,
json_path,
json: compact_json,
linker_script_body,
})
}
#[cfg(target_os = "linux")]
fn validate_embedded_json(desc_json: &str) -> ModuleInfoResult<()> {
if desc_json.len() > constants::MAX_JSON_SIZE {
return Err(ModuleInfoError::MetadataTooLarge(format!(
"Metadata size {} exceeds limit of {} bytes",
desc_json.len(),
constants::MAX_JSON_SIZE
)));
}
let value: serde_json::Value = serde_json::from_str(desc_json)
.map_err(|e| ModuleInfoError::MalformedJson(e.to_string()))?;
if !value.is_object() {
return Err(ModuleInfoError::MalformedJson(
"Metadata must be a JSON object".to_string(),
));
}
for field in constants::REQUIRED_JSON_KEYS {
let present_and_nonempty = value
.get(field)
.and_then(|v| v.as_str())
.map(|s| !s.is_empty())
.unwrap_or(false);
if !present_and_nonempty {
return Err(ModuleInfoError::MalformedJson(format!(
"Required field '{field}' is missing or empty"
)));
}
}
let mv = value
.get("moduleVersion")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ModuleInfoError::MalformedJson(
"moduleVersion must be a non-empty string by this point (required-keys check above enforces it)"
.to_string(),
)
})?;
validate_module_version(mv)?;
Ok(())
}
#[cfg(target_os = "linux")]
fn emit_metadata_notes(desc_json: &str) {
let map = match serde_json::from_str::<serde_json::Value>(desc_json) {
Ok(serde_json::Value::Object(map)) => map,
Ok(other) => {
debug!("emit_metadata_notes: expected a JSON object, got {}", other);
return;
}
Err(e) => {
debug!("emit_metadata_notes: JSON parse failed: {}", e);
return;
}
};
for field in ModuleInfoField::ALL {
let key = field.to_key();
if let Some(value) = map.get(key) {
match value.as_str() {
Some(s) => note!("{}: {}", key, s),
None => note!("{}: {}", key, value.to_string()),
}
}
}
}
#[cfg(target_os = "linux")]
fn link_arg_directive(linker_script_path: &Path, emit: bool) -> Option<String> {
if emit {
Some(format!(
"cargo:rustc-link-arg=-T{}",
linker_script_path.display()
))
} else {
None
}
}
#[cfg(target_os = "linux")]
fn emit_rerun_if_directives() {
for path in [
"Cargo.toml",
"build.rs",
".git/HEAD",
".git/refs",
".git/packed-refs",
"/etc/os-release",
] {
println!("cargo:rerun-if-changed={path}");
}
for env_var in ["MODULE_INFO_DEBUG", "CARGO_PKG_NAME", "CARGO_PKG_VERSION"] {
println!("cargo:rerun-if-env-changed={env_var}");
}
}
#[cfg(target_os = "linux")]
#[must_use = "build.rs must propagate errors from this function, otherwise a missing linker script will silently break the ELF note section"]
pub fn generate_project_metadata_and_linker_script() -> Result<(), Box<dyn std::error::Error>> {
let md = PackageMetadata::from_cargo_toml().map_err(|e| {
error!("Failed to get project metadata: {}", e);
e
})?;
let artifacts = embed_package_metadata(&md, &EmbedOptions::default())?;
debug!(
"Wrote linker script: {}",
artifacts.linker_script_path.display()
);
Ok(())
}
#[cfg(not(target_os = "linux"))]
#[must_use = "build.rs must propagate errors from this function, otherwise a missing linker script will silently break the ELF note section"]
pub fn generate_project_metadata_and_linker_script() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(all(feature = "embed-module-info", target_os = "linux"))]
#[must_use = "print_module_info returns a Result indicating whether the embedded note section was readable; ignoring it will hide missing-metadata errors"]
pub fn print_module_info() -> ModuleInfoResult<()> {
let info = get_module_info!()?;
let missing: Vec<&str> = constants::REQUIRED_JSON_KEYS
.iter()
.filter(|key| info.get(**key).map_or(true, |v| v.is_empty()))
.copied()
.collect();
if !missing.is_empty() {
return Err(ModuleInfoError::NotAvailable(format!(
"Module info appears to be missing or corrupted: required field(s) missing or empty: {}",
missing.join(", ")
)));
}
for field in ModuleInfoField::ALL {
let key = field.to_key();
match info.get(key) {
Some(value) => println!("{key}: {value}"),
None => println!("{key}: <unavailable>"),
}
}
Ok(())
}
#[cfg(any(not(feature = "embed-module-info"), not(target_os = "linux")))]
#[must_use = "print_module_info returns a Result indicating whether the embedded note section was readable; ignoring it will hide missing-metadata errors"]
pub fn print_module_info() -> ModuleInfoResult<()> {
Err(ModuleInfoError::NotAvailable(
"Module info is only available on Linux platforms with the embed-module-info feature enabled.".to_string(),
))
}
#[cfg(feature = "embed-module-info")]
#[must_use = "get_version returns the embedded version string; discarding it hides missing-metadata errors"]
pub fn get_version() -> ModuleInfoResult<String> {
get_module_info!(ModuleInfoField::Version)
}
#[cfg(feature = "embed-module-info")]
#[must_use = "get_module_version returns the embedded 4-part module version; discarding it hides missing-metadata errors"]
pub fn get_module_version() -> ModuleInfoResult<String> {
get_module_info!(ModuleInfoField::ModuleVersion)
}
#[cfg(all(feature = "embed-module-info", target_os = "linux"))]
#[must_use = "extract_module_info returns the parsed field value; discarding it defeats the point of calling it"]
pub unsafe fn extract_module_info(ptr: *const u8) -> ModuleInfoResult<String> {
if ptr.is_null() {
return Err(ModuleInfoError::NullPointer);
}
const MAX_NOTE_VALUE_LEN: usize = constants::MAX_JSON_SIZE + constants::NOTE_ALIGN;
let mut open_quote: Option<usize> = None;
for i in 0..MAX_NOTE_VALUE_LEN {
let byte = unsafe { *ptr.add(i) };
if byte == 0 {
let message = if open_quote.is_none() {
"Unexpected NUL before opening quote of JSON value"
} else {
"Unexpected NUL before closing quote of JSON value"
};
return Err(ModuleInfoError::MalformedJson(message.to_string()));
}
if byte == b'"' {
match open_quote {
None => open_quote = Some(i),
Some(open) => {
let len = i - open - 1;
let bytes = unsafe { std::slice::from_raw_parts(ptr.add(open + 1), len) };
let value = std::str::from_utf8(bytes)?;
return Ok(value.to_string());
}
}
}
}
let detail = if open_quote.is_none() {
"no opening quote found"
} else {
"opening quote found but no closing quote"
};
Err(ModuleInfoError::MalformedJson(format!(
"{detail} within {MAX_NOTE_VALUE_LEN} bytes; \
.note.package section is missing, stripped, or corrupted"
)))
}
#[cfg(all(feature = "embed-module-info", not(target_os = "linux")))]
#[must_use = "extract_module_info returns the parsed field value; discarding it defeats the point of calling it"]
pub unsafe fn extract_module_info(_ptr: *const u8) -> ModuleInfoResult<String> {
Err(ModuleInfoError::NotAvailable(
"Extract module info is only available on Linux platforms with embed-module-info feature."
.to_string(),
))
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use std::{error::Error, fs::File, io::Read, path::Path};
use tempfile::NamedTempFile;
use super::*;
type TestResult = Result<(), Box<dyn Error>>;
fn git_is_available() -> bool {
match std::process::Command::new("git")
.arg("--version")
.stdin(std::process::Stdio::null())
.output()
{
Ok(output) => output.status.success(),
Err(_) => false,
}
}
#[cfg(feature = "embed-module-info")]
#[test]
#[allow(clippy::unnecessary_cast)]
fn test_extract_module_info() -> TestResult {
let test_str = "\"test_value\"";
let c_str = std::ffi::CString::new(test_str)?;
let ptr = c_str.as_ptr() as *const u8;
let value = unsafe { extract_module_info(ptr) }?;
assert_eq!(value, "test_value");
Ok(())
}
#[cfg(feature = "embed-module-info")]
#[test]
fn extract_module_info_stops_at_closing_quote() -> TestResult {
let bytes: Vec<u8> = b"\"hello\",\n\"version\":\"1.2.3\"\0".to_vec();
let value = unsafe { extract_module_info(bytes.as_ptr()) }?;
assert_eq!(
value, "hello",
"scan must stop at the closing quote, not walk past into the next field"
);
Ok(())
}
#[cfg(feature = "embed-module-info")]
#[test]
fn extract_module_info_rejects_leading_nul() {
let bytes: [u8; 4] = [0, 0, 0, 0];
match unsafe { extract_module_info(bytes.as_ptr()) } {
Err(ModuleInfoError::MalformedJson(msg)) => assert!(
msg.contains("NUL"),
"error must mention the NUL trigger: {msg}"
),
other => panic!("expected MalformedJson(...NUL...), got {other:?}"),
}
}
#[cfg(feature = "embed-module-info")]
#[test]
fn extract_module_info_reports_cap_on_runaway_scan() {
let mut bytes = vec![b'a'; 2048];
bytes[0] = b'"';
match unsafe { extract_module_info(bytes.as_ptr()) } {
Err(ModuleInfoError::MalformedJson(msg)) => assert!(
msg.contains("missing, stripped, or corrupted"),
"cap-hit error must keep the diagnostic phrasing: {msg}"
),
other => panic!("expected MalformedJson(...corrupted...), got {other:?}"),
}
}
#[test]
fn test_align_len() {
assert_eq!(utils::align_len(5, NOTE_ALIGN), 8);
assert_eq!(utils::align_len(8, NOTE_ALIGN), 8);
assert_eq!(utils::align_len(9, NOTE_ALIGN), 12);
}
#[test]
fn align_len_saturates_on_u32_overflow() {
assert_eq!(utils::align_len(u32::MAX, 4), u32::MAX);
assert_eq!(utils::align_len(u32::MAX - 1, 4), u32::MAX);
}
#[test]
fn note_section_rejects_short_owner() {
use crate::note_section::NoteSection;
match NoteSection::new(N_TYPE, "", "desc", "", NOTE_ALIGN) {
Err(ModuleInfoError::Other(boxed)) => assert!(
boxed.to_string().contains("n_namesz"),
"diagnostic must name the field: {boxed}"
),
Err(other) => panic!("expected Other(...n_namesz...), got {other:?}"),
Ok(_) => panic!("empty owner must be rejected"),
}
match NoteSection::new(N_TYPE, "AB", "desc", "", NOTE_ALIGN) {
Err(ModuleInfoError::Other(_)) => {}
Err(other) => panic!("expected Other(_), got {other:?}"),
Ok(_) => panic!("two-byte owner must be rejected"),
}
}
#[test]
fn validate_embedded_json_rejects_oversized_payload() {
let big_value = "x".repeat(constants::MAX_JSON_SIZE + 16);
let json = format!(r#"{{"binary":"{big_value}"}}"#);
let err = validate_embedded_json(&json)
.expect_err("payloads over MAX_JSON_SIZE must be rejected");
match err {
ModuleInfoError::MetadataTooLarge(msg) => assert!(
msg.contains("exceeds limit"),
"diagnostic must mention the cap: {msg}"
),
other => panic!("expected MetadataTooLarge, got {other:?}"),
}
}
#[test]
fn validate_embedded_json_rejects_non_object_shapes() {
for bad in ["[]", "null", "42", r#""string""#] {
let err = validate_embedded_json(bad).expect_err("non-object JSON must be rejected");
assert!(
matches!(err, ModuleInfoError::MalformedJson(_)),
"expected MalformedJson for {bad:?}"
);
}
}
#[cfg(feature = "embed-module-info")]
#[test]
fn new_one_call_entry_point_writes_artifacts() -> TestResult {
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let tmp = tempfile::tempdir()?;
let prior = std::env::var_os("OUT_DIR");
std::env::set_var("OUT_DIR", tmp.path());
let result = new(Info {
binary: "one_call_test".into(),
name: "one_call_test".into(),
version: "1.0.0".into(),
moduleVersion: "1.0.0.0".into(),
maintainer: "team@contoso.com".into(),
os: "linux".into(),
osVersion: "test".into(),
..Default::default()
});
match prior {
Some(p) => std::env::set_var("OUT_DIR", p),
None => std::env::remove_var("OUT_DIR"),
}
let artifacts = result?;
assert!(artifacts.linker_script_path.starts_with(tmp.path()));
assert!(artifacts.json_path.exists());
let parsed: serde_json::Value = serde_json::from_str(&artifacts.json)?;
assert_eq!(parsed["binary"], "one_call_test");
Ok(())
}
#[test]
fn test_get_distro_info() -> TestResult {
use crate::utils::get_distro_info;
let distro_info = get_distro_info()?;
assert!(!distro_info.0.is_empty());
assert!(!distro_info.1.is_empty());
Ok(())
}
#[test]
fn note_section_is_4byte_aligned_for_every_residue() {
use crate::note_section::NoteSection;
for desc_len in [0usize, 1, 2, 3, 4, 5, 7, 8, 17, 100, 1023] {
let desc = "x".repeat(desc_len);
let note = match NoteSection::new(N_TYPE, OWNER, &desc, "", NOTE_ALIGN) {
Ok(n) => n,
Err(e) => panic!("NoteSection::new failed for desc_len={desc_len}: {e}"),
};
assert_eq!(
note.note_section.len() % NOTE_ALIGN,
0,
"note section must be 4-byte aligned (desc_len={desc_len}, got {})",
note.note_section.len()
);
}
}
#[test]
fn test_project_metadata() {
if !git_is_available() {
println!("Skipping test_project_metadata because git cli is not available");
return;
}
use crate::metadata::project_metadata;
let result = project_metadata();
assert!(
result.is_ok(),
"Project metadata should be created successfully: {:?}",
result.err()
);
if let Ok(res) = result {
let metadata = res.0;
assert!(
metadata.contains("\"binary\":"),
"JSON should contain binary field"
);
assert!(
metadata.contains("\"moduleVersion\":"),
"JSON should contain moduleVersion field"
);
assert!(
metadata.contains("\"version\":"),
"JSON should contain version field"
);
assert!(
metadata.contains("\"maintainer\":"),
"JSON should contain maintainer field"
);
assert!(
metadata.contains("\"name\":"),
"JSON should contain name field"
);
assert!(
metadata.contains("\"type\":"),
"JSON should contain type field"
);
assert!(
metadata.contains("\"repo\":") || metadata.contains("\"Unknown\""),
"JSON should contain repo field or fallback"
);
assert!(
metadata.contains("\"branch\":")
|| metadata.contains("\"main\"")
|| metadata.contains("\"unknown\""),
"JSON should contain branch field or fallback"
);
assert!(
metadata.contains("\"hash\":") || metadata.contains("\"unknown\""),
"JSON should contain hash field or fallback"
);
assert!(
metadata.contains("\"copyright\":"),
"JSON should contain copyright field"
);
assert!(metadata.contains("\"os\":"), "JSON should contain os field");
assert!(
metadata.contains("\"osVersion\":"),
"JSON should contain osVersion field"
);
}
}
#[test]
fn test_package_metadata_from_cargo_toml() -> TestResult {
let md = PackageMetadata::from_cargo_toml()?;
assert_eq!(md.name, "module-info");
assert_eq!(md.binary, "module-info");
let parts: Vec<&str> = md.version.split('.').collect();
assert_eq!(
parts.len(),
3,
"version should have three dot-separated parts, got {:?}",
md.version
);
for part in &parts {
assert!(
part.chars().all(|c| c.is_ascii_digit()),
"version part {part:?} must be numeric"
);
}
assert!(
!md.copyright.is_empty() && md.copyright != "Unknown",
"copyright must come from Cargo.toml, not the Unknown fallback; got {:?}",
md.copyright
);
Ok(())
}
#[test]
fn test_get_git_info() -> TestResult {
if !git_is_available() {
println!("Skipping test_get_git_info because git is not available");
return Ok(());
}
use crate::utils::get_git_info;
let git_info = get_git_info()?;
assert!(!git_info.0.is_empty(), "Branch name should not be empty"); assert!(!git_info.1.is_empty(), "Commit hash should not be empty"); assert!(
!git_info.2.is_empty(),
"Repository name should not be empty"
);
assert!(git_info.2 == "unknown" || !git_info.2.is_empty());
println!(
"Git Info - Branch: {}, Hash: {}, Repo: {}",
git_info.0, git_info.1, git_info.2
);
Ok(())
}
#[test]
fn test_json_key_value_parse() -> TestResult {
let json_input = r#"{
"binary": "sample_crashing_process",
"moduleVersion": "0.1.0.0",
"version": "0.1.0",
"maintainer": "Maintainer contact/UUID etc",
"name": "sample_crashing_process",
"type": "agent",
"repo": "Module_Info",
"branch": "main",
"hash": "76930c41aa16e31bb1e565b12c4285cde1939af3",
"copyright": "Microsoft",
"os": "Ubuntu",
"osVersion": "20.04"
}
"#;
let parsed: serde_json::Value = serde_json::from_str(json_input)?;
assert_eq!(parsed["binary"], "sample_crashing_process");
assert_eq!(parsed["moduleVersion"], "0.1.0.0");
assert_eq!(parsed["version"], "0.1.0");
assert_eq!(parsed["maintainer"], "Maintainer contact/UUID etc");
assert_eq!(parsed["name"], "sample_crashing_process");
assert_eq!(parsed["type"], "agent");
assert_eq!(parsed["repo"], "Module_Info");
assert_eq!(parsed["branch"], "main");
assert_eq!(parsed["hash"], "76930c41aa16e31bb1e565b12c4285cde1939af3");
assert_eq!(parsed["copyright"], "Microsoft");
assert_eq!(parsed["os"], "Ubuntu");
assert_eq!(parsed["osVersion"], "20.04");
Ok(())
}
#[test]
fn test_get_project_path() {
use crate::utils::get_project_path;
let project_path = get_project_path();
assert!(project_path.exists());
}
#[test]
fn test_get_cargo_toml_content() -> TestResult {
use crate::utils::get_cargo_toml_content;
let cargo_toml = get_cargo_toml_content()?;
assert!(cargo_toml.get("package").is_some());
Ok(())
}
#[test]
fn test_save_section() -> TestResult {
let temp_file = NamedTempFile::new()?;
let file_path = temp_file.path().to_path_buf();
let desc_json = r#"{"binary":"test","version":"1.0.0"}"#;
let linker_script_body = "BYTE(0x01); BYTE(0x02);";
use crate::note_section::NoteSection;
let note = NoteSection::new(N_TYPE, OWNER, desc_json, linker_script_body, NOTE_ALIGN)?;
note.save_section(&file_path)?;
let mut file = File::open(&file_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
assert!(!buffer.is_empty());
assert_eq!(buffer.len(), note.note_section.len());
assert_eq!(buffer, note.note_section);
assert!(buffer.len() >= 12);
let owner_offset = 12; let owner_bytes = OWNER.as_bytes();
let owner_slice = buffer
.get(owner_offset..owner_offset + owner_bytes.len())
.ok_or("owner slice is out of bounds")?;
assert_eq!(owner_slice, owner_bytes);
let n_type_bytes = N_TYPE.to_le_bytes();
let n_type_slice = buffer.get(8..12).ok_or("n_type slice is out of bounds")?;
assert_eq!(n_type_slice, &n_type_bytes);
Ok(())
}
#[test]
fn test_package_metadata_default_construction() {
let md = PackageMetadata {
binary: "my_tool".into(),
name: "my_tool".into(),
version: "1.2.3".into(),
module_version: "1.2.3.4".into(),
maintainer: "team@contoso.com".into(),
hash: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".into(),
..Default::default()
};
assert_eq!(md.binary, "my_tool");
assert_eq!(md.version, "1.2.3");
assert_eq!(md.module_version, "1.2.3.4");
assert_eq!(md.module_type, "");
assert_eq!(md.repo, "");
assert_eq!(md.os, "");
}
#[cfg(feature = "embed-module-info")]
#[test]
fn test_embed_package_metadata_custom_out_dir_no_link_arg() -> TestResult {
let tmp = tempfile::tempdir()?;
let md = PackageMetadata {
binary: "test_binary".into(),
name: "test_binary".into(),
version: "1.2.3".into(),
module_version: "1.2.3.4".into(),
maintainer: "team@contoso.com".into(),
hash: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".into(),
module_type: "agent".into(),
repo: "test_repo".into(),
branch: "main".into(),
copyright: "Test".into(),
os: "Ubuntu".into(),
os_version: "22.04".into(),
..Default::default()
};
let opts = EmbedOptions {
out_dir: Some(tmp.path().to_path_buf()),
emit_cargo_link_arg: false,
..Default::default()
};
let artifacts = embed_package_metadata(&md, &opts)?;
assert!(artifacts.linker_script_path.starts_with(tmp.path()));
assert!(artifacts.note_bin_path.starts_with(tmp.path()));
assert!(artifacts.json_path.starts_with(tmp.path()));
assert!(artifacts.linker_script_path.exists());
assert!(artifacts.note_bin_path.exists());
assert!(artifacts.json_path.exists());
let parsed: serde_json::Value = serde_json::from_str(&artifacts.json)?;
assert_eq!(parsed["binary"], "test_binary");
assert_eq!(parsed["version"], "1.2.3");
assert_eq!(parsed["moduleVersion"], "1.2.3.4");
Ok(())
}
#[cfg(feature = "embed-module-info")]
#[test]
fn test_embed_package_metadata_rejects_empty_required_field() -> TestResult {
let tmp = tempfile::tempdir()?;
let md = PackageMetadata {
binary: "b".into(),
name: "n".into(),
version: "1.0.0".into(),
module_version: "1.0.0.0".into(),
maintainer: "m".into(),
os: "linux".into(),
..Default::default()
};
let opts = EmbedOptions {
out_dir: Some(tmp.path().to_path_buf()),
emit_cargo_link_arg: false,
..Default::default()
};
let err = embed_package_metadata(&md, &opts)
.expect_err("embed must reject PackageMetadata with empty required field");
match err {
ModuleInfoError::MalformedJson(msg) => {
assert!(
msg.contains("osVersion"),
"error must name the empty required field: {msg}"
);
}
other => panic!("expected MalformedJson, got {other:?}"),
}
Ok(())
}
#[test]
fn test_validate_embedded_json_rejects_missing_required_fields() {
let bad_json = r#"{"binary":"b","version":"1.0.0","moduleVersion":"1.0.0.0","name":"n"}"#;
let err =
validate_embedded_json(bad_json).expect_err("missing required field must be rejected");
match err {
ModuleInfoError::MalformedJson(msg) => {
assert!(
msg.contains("maintainer"),
"error must name the missing field: {msg}"
);
}
other => panic!("expected MalformedJson, got {other:?}"),
}
}
#[test]
fn test_validate_embedded_json_rejects_empty_required_fields() {
let bad_json = r#"{"binary":"b","version":"1.0.0","moduleVersion":"1.0.0.0","name":"n","maintainer":""}"#;
let err =
validate_embedded_json(bad_json).expect_err("empty required field must be rejected");
match err {
ModuleInfoError::MalformedJson(msg) => {
assert!(
msg.contains("maintainer"),
"error must name the empty field: {msg}"
);
}
other => panic!("expected MalformedJson, got {other:?}"),
}
}
#[test]
fn test_validate_embedded_json_accepts_empty_optional_fields() {
let ok_json = r#"{"binary":"b","version":"1.0.0","moduleVersion":"1.0.0.0","name":"n","maintainer":"m","type":"","repo":"","branch":"","hash":"","copyright":"","os":"linux","osVersion":"1"}"#;
if let Err(e) = validate_embedded_json(ok_json) {
panic!("optional fields may be empty; only the identity keys are required. got {e:?}");
}
}
#[test]
fn test_embed_options_default_preserves_bc_behavior() {
let opts = EmbedOptions::default();
assert!(opts.out_dir.is_none());
assert!(opts.emit_cargo_link_arg);
}
#[test]
fn render_note_payloads_always_emits_nul_padding() -> TestResult {
for suffix_len in 0..=4 {
let suffix = "x".repeat(suffix_len);
let md = PackageMetadata {
binary: format!("b{suffix}"),
name: format!("n{suffix}"),
version: "1.0.0".into(),
module_version: "1.0.0.0".into(),
maintainer: "m".into(),
os: "linux".into(),
os_version: "22.04".into(),
..Default::default()
};
let (_json, linker_script_body) = crate::metadata::render_note_payloads(&md)?;
assert!(
linker_script_body.contains("BYTE(0x00);"),
"linker script must contain a BYTE(0x00) even when the payload is 4-aligned (suffix_len={suffix_len})"
);
}
Ok(())
}
#[test]
fn link_arg_directive_gates_on_flag() {
let p = Path::new("/tmp/linker_script.ld");
match link_arg_directive(p, true) {
Some(d) => assert_eq!(d, "cargo:rustc-link-arg=-T/tmp/linker_script.ld"),
None => panic!("emit_cargo_link_arg=true must produce a directive"),
}
assert!(
link_arg_directive(p, false).is_none(),
"emit_cargo_link_arg=false must suppress the directive"
);
}
#[test]
fn required_keys_are_subset_of_module_info_fields() {
let known: std::collections::HashSet<&str> =
ModuleInfoField::ALL.iter().map(|f| f.to_key()).collect();
for key in constants::REQUIRED_JSON_KEYS {
assert!(
known.contains(key),
"REQUIRED_JSON_KEYS contains {key:?} which is not in ModuleInfoField::ALL"
);
}
}
#[test]
fn info_struct_literal_and_conversion() {
let info = Info {
binary: "b".into(),
version: "1.2.3".into(),
moduleVersion: "1.2.3.4".into(),
maintainer: "m".into(),
name: "n".into(),
r#type: "agent".into(),
repo: "r".into(),
branch: "br".into(),
hash: "h".into(),
copyright: "c".into(),
os: "o".into(),
osVersion: "ov".into(),
};
let md: PackageMetadata = info.into();
assert_eq!(md.binary, "b");
assert_eq!(md.version, "1.2.3");
assert_eq!(md.module_version, "1.2.3.4");
assert_eq!(md.maintainer, "m");
assert_eq!(md.name, "n");
assert_eq!(md.module_type, "agent");
assert_eq!(md.repo, "r");
assert_eq!(md.branch, "br");
assert_eq!(md.hash, "h");
assert_eq!(md.copyright, "c");
assert_eq!(md.os, "o");
assert_eq!(md.os_version, "ov");
}
#[test]
fn info_default_fills_missing_fields_with_empty_strings() {
let info = Info {
binary: "b".into(),
moduleVersion: "1.2.3.4".into(),
..Default::default()
};
assert_eq!(info.binary, "b");
assert_eq!(info.moduleVersion, "1.2.3.4");
assert_eq!(info.version, "");
assert_eq!(info.r#type, "");
assert_eq!(info.osVersion, "");
}
#[cfg(feature = "embed-module-info")]
#[test]
fn info_embed_round_trip_writes_artifacts() -> TestResult {
let tmp = tempfile::tempdir()?;
let md: PackageMetadata = Info {
binary: "b".into(),
name: "n".into(),
version: "1.2.3".into(),
moduleVersion: "1.2.3.4".into(),
maintainer: "m".into(),
r#type: "agent".into(),
hash: "deadbeef".into(),
os: "linux".into(),
osVersion: "22.04".into(),
..Default::default()
}
.into();
let opts = EmbedOptions {
out_dir: Some(tmp.path().to_path_buf()),
emit_cargo_link_arg: false,
..Default::default()
};
let artifacts = embed_package_metadata(&md, &opts)?;
assert!(artifacts.linker_script_path.starts_with(tmp.path()));
assert!(artifacts.json_path.exists());
let parsed: serde_json::Value = serde_json::from_str(&artifacts.json)?;
assert_eq!(parsed["moduleVersion"], "1.2.3.4");
assert_eq!(parsed["type"], "agent");
Ok(())
}
#[test]
fn validate_module_version_accepts_valid_values() -> TestResult {
for v in ["0.0.0.0", "1.2.3.4", "65535.65535.65535.65535", "10.0.0.1"] {
validate_module_version(v)?;
}
Ok(())
}
#[test]
fn validate_module_version_rejects_wrong_part_count() {
for v in ["", "1", "1.2", "1.2.3", "1.2.3.4.5"] {
let err = validate_module_version(v).expect_err("wrong part count must be rejected");
match err {
ModuleInfoError::MalformedJson(msg) => {
assert!(
msg.contains("exactly 4"),
"error must explain the 4-part rule: {msg}"
);
}
other => panic!("expected MalformedJson, got {other:?}"),
}
}
}
#[test]
fn validate_module_version_rejects_overflow() {
for v in [
"65536.0.0.0",
"0.65536.0.0",
"0.0.65536.0",
"0.0.0.65536",
"99999.1.2.3",
] {
let err = validate_module_version(v).expect_err("u16 overflow must be rejected");
match err {
ModuleInfoError::MalformedJson(msg) => {
assert!(
msg.contains("16 bits"),
"error must mention the u16 constraint: {msg}"
);
}
other => panic!("expected MalformedJson, got {other:?}"),
}
}
}
#[test]
fn validate_module_version_rejects_non_numeric() {
for v in ["-1.0.0.0", "a.b.c.d", "1.2.x.4", "1.2.3.4a", "v1.2.3.4"] {
validate_module_version(v).expect_err("non-numeric parts must be rejected");
}
}
#[test]
fn validate_module_version_rejects_empty_part() {
for v in ["1.2.3.", "1..3.4", "..1.2", "1.2..4"] {
let err = validate_module_version(v).expect_err("empty part must be rejected");
if let ModuleInfoError::MalformedJson(msg) = err {
assert!(
msg.contains("empty") || msg.contains("exactly 4"),
"unexpected error message: {msg}"
);
} else {
panic!("expected MalformedJson");
}
}
}
#[test]
fn validate_embedded_json_rejects_bad_module_version() {
let bad_json = r#"{"binary":"b","version":"1.0.0","moduleVersion":"1.2.3.99999","name":"n","maintainer":"m","os":"linux","osVersion":"22.04"}"#;
let err = validate_embedded_json(bad_json)
.expect_err("out-of-range moduleVersion must be rejected");
match err {
ModuleInfoError::MalformedJson(msg) => {
assert!(
msg.contains("moduleVersion"),
"error must name the field: {msg}"
);
}
other => panic!("expected MalformedJson, got {other:?}"),
}
}
#[test]
fn package_metadata_field_value_covers_all_variants() -> TestResult {
let md = PackageMetadata {
binary: "bv".into(),
version: "vv".into(),
module_version: "mv".into(),
maintainer: "mn".into(),
name: "nv".into(),
module_type: "tv".into(),
repo: "rv".into(),
branch: "bn".into(),
hash: "hv".into(),
copyright: "cv".into(),
os: "ov".into(),
os_version: "ov2".into(),
};
let json: serde_json::Value = serde_json::from_str(&serde_json::to_string(&md)?)?;
for field in ModuleInfoField::ALL {
let from_method = md.field_value(*field);
let from_json = json
.get(field.to_key())
.and_then(|v| v.as_str())
.unwrap_or_else(|| panic!("JSON missing key for {field:?}"));
assert_eq!(
from_method, from_json,
"field_value and serde output disagree for {field:?}"
);
}
Ok(())
}
}