blob: cc3039eced105139745ba43a178519ea2ea06c39 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-only
//
// Copyright (c) 2025 - Sasha Levin <sashal@kernel.org>
use anyhow::{Context, Result};
use clap::Parser;
use cve_utils::git_config;
use cve_utils::git_utils::{get_object_full_sha, resolve_reference, get_affected_files};
use git2::Repository;
use log::{debug, error, warn};
use std::env;
use std::path::{Path, PathBuf};
mod commands;
mod models;
mod utils;
use commands::{generate_json, generate_mbox, json::CveRecordParams, mbox::MboxParams};
use models::{Args, DyadEntry};
use utils::{
get_commit_subject, get_commit_text, read_message_file, read_tags_file,
run_dyad, strip_commit_text,
};
/// Initialize and configure the logging system
fn initialize_logging(verbose: bool) -> log::LevelFilter {
let logging_level = if verbose {
log::LevelFilter::max()
} else {
log::LevelFilter::Error
};
env_logger::builder()
.format_timestamp(None)
.filter_level(logging_level)
.init();
logging_level
}
/// Log command line arguments if in verbose mode
fn log_command_args() {
if std::env::args().len() > 0 {
debug!("Raw command line arguments:");
for (i, arg) in std::env::args().enumerate() {
debug!(" Arg[{i}]: '{arg}'");
}
}
}
/// Type alias for argument validation return value
type ArgsResult = (String, String, String, Vec<String>, Vec<String>);
/// Validate command line arguments and environment variables
fn validate_args_and_env(args: &Args) -> ArgsResult {
// We should not have ANY trailing arguments, so if we do, print them out and abort
if !args.remaining_parameters.is_empty() {
error!("Trailing arguments detected:");
for (i, arg) in args.remaining_parameters.iter().enumerate() {
error!(" trailing_arg[{i}]: '{arg}'");
}
std::process::exit(1);
}
// Check for required arguments
if args.cve.is_none() {
error!("Missing required argument: cve");
std::process::exit(1);
}
if args.sha.is_empty() {
error!("Missing required argument: sha");
std::process::exit(1);
}
if args.json.is_none() && args.mbox.is_none() {
error!("Missing required argument: one of json or mbox must be specified");
std::process::exit(1);
}
// Check for CVE_USER environment variable if user is not specified
let user_email = args.user.as_ref().map_or_else(
|| {
env::var("CVE_USER").unwrap_or_else(|_| {
error!("Missing required argument: user (-u/--user) and CVE_USER environment variable is not set");
std::process::exit(1);
})
},
std::clone::Clone::clone
);
// Check for CVEKERNELTREE environment variable
if env::var("CVEKERNELTREE").is_err() {
error!("CVEKERNELTREE environment variable is not set");
error!("It needs to be set to the stable repo directory");
std::process::exit(1);
}
// Extract values from args
let cve_number = args.cve.as_ref().unwrap().clone();
let git_shas: Vec<String> = args
.sha
.iter()
.filter(|s| !s.trim().is_empty())
.cloned()
.collect();
if git_shas.is_empty() {
error!("Missing required argument: sha");
std::process::exit(1);
}
// Use all provided vulnerable SHAs (if any)
let vulnerable_shas: Vec<String> = args
.vulnerable
.iter()
.filter(|s| !s.trim().is_empty())
.cloned()
.collect();
// Dig into git if the user name is not set
let user_name = args
.name
.clone()
.unwrap_or_else(|| git_config::get_git_config("user.name").unwrap_or_default());
// Debug output if verbose is enabled
debug!("CVE_NUMBER={cve_number}");
debug!("GIT_SHAS={git_shas:?}");
debug!("JSON_FILE={:?}", args.json);
debug!("MBOX_FILE={:?}", args.mbox);
debug!("REFERENCE_FILE={:?}", args.reference);
debug!("GIT_VULNERABLE={vulnerable_shas:?}");
(cve_number, user_name, user_email, git_shas, vulnerable_shas)
}
/// Get repository and script directories
fn get_directories() -> Result<(String, PathBuf, String, String)> {
// Get vulns directory using cve_utils
let vulns_dir =
cve_utils::find_vulns_dir().with_context(|| "Failed to find vulns directory")?;
// Get scripts directory
let script_dir = vulns_dir.join("scripts");
if !script_dir.exists() {
return Err(anyhow::anyhow!(
"Scripts directory not found at {}",
script_dir.display()
));
}
// Get the script name
let script_name = "bippy".to_string();
// Get the script version using Cargo package version
let script_version = env!("CARGO_PKG_VERSION").to_string();
// Get kernel tree path from environment
let kernel_tree = env::var("CVEKERNELTREE")
.with_context(|| "CVEKERNELTREE environment variable is not set")?;
Ok((kernel_tree, script_dir, script_name, script_version))
}
/// Get commit information from Git repository
fn get_commit_info(kernel_tree: &str, git_shas: &[String]) -> Result<(String, String, String, Vec<String>)> {
// Open the kernel repository
let repo = Repository::open(kernel_tree)
.with_context(|| format!("Failed to open Git repository at {kernel_tree:?}"))?;
// Resolve Git references for all main commits
let git_refs: Vec<_> = git_shas
.iter()
.filter_map(|sha| match resolve_reference(&repo, sha) {
Ok(reference) => Some(reference),
Err(err) => {
warn!("Warning: Could not resolve SHA reference: {err}");
None
}
})
.collect();
if git_refs.is_empty() {
error!("None of the provided SHAs could be resolved");
std::process::exit(1);
}
// Use the first as the main one for output fields
let main_git_ref = &git_refs[0];
// Get SHA information for the main commit
let git_sha_full =
get_object_full_sha(&repo, main_git_ref).with_context(|| "Failed to get full SHA")?;
let commit_subject =
get_commit_subject(&repo, main_git_ref).with_context(|| "Failed to get commit subject")?;
// Get the full commit message text for the main commit
let git_ref = resolve_reference(&repo, &git_sha_full)?;
let commit_text = get_commit_text(&repo, &git_ref)?;
// Get the affected files list
let affected_files = get_affected_files(&repo, &git_ref)?;
Ok((git_sha_full, commit_subject, commit_text, affected_files))
}
/// Process the commit text
fn process_commit_text(script_dir: &Path, commit_text: &str, message_file: Option<&Path>) -> String {
// Check if a message file was explicitly provided
if let Some(message_path) = message_file {
if let Ok(Some(message_content)) = read_message_file(message_path) {
let trimmed_content = message_content.trim();
// Check if message file has actual content after trimming
if !trimmed_content.is_empty() {
debug!("Using content from .message file: {}", message_path.display());
// When using a .message file, use its content as the complete description
return format!("In the Linux kernel, the following vulnerability has been resolved:\n\n{}", trimmed_content);
} else {
error!("Warning: Message file {} is empty or contains only whitespace, falling back to commit message", message_path.display());
}
} else {
error!("Warning: Could not read message file: {}", message_path.display());
}
}
// Read the tags file to strip from commit message
let tags = read_tags_file(script_dir).unwrap_or_default();
// Strip tags from commit text normally
strip_commit_text(commit_text, &tags)
}
/// Run dyad and parse its output into `DyadEntry` objects
fn run_dyad_and_parse(
script_dir: &Path,
git_shas: &[String],
vulnerable_shas: &[String],
) -> Vec<DyadEntry> {
let dyad_data = match run_dyad(script_dir, git_shas, vulnerable_shas) {
Ok(data) => data,
Err(err) => {
warn!("Warning: Failed to run dyad: {err:?}");
String::new()
}
};
// Parse dyad output into DyadEntry objects
let mut dyad_entries: Vec<DyadEntry> = Vec::new();
// Process dyad data to create entries
if !dyad_data.is_empty() {
for line in dyad_data.lines() {
// Skip comments and empty lines
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
// Parse the line directly as DyadEntry
if let Ok(entry) = DyadEntry::from_str(line) {
dyad_entries.push(entry);
}
}
}
dyad_entries
}
/// Read additional references from a file if specified
fn read_additional_references(reference_path: Option<PathBuf>) -> Vec<String> {
reference_path.map_or_else(
|| {
debug!("No reference file specified");
Vec::new()
},
|ref_path| {
debug!("Attempting to read references from {}", ref_path.display());
std::fs::read_to_string(&ref_path).map_or_else(
|_| {
warn!("Warning: Failed to read reference file from {}", ref_path.display());
if !ref_path.exists() {
debug!(" File does not exist");
} else if !ref_path.is_file() {
debug!(" Path exists but is not a regular file");
} else {
debug!(" File exists but could not be read (permissions issue?)");
}
Vec::new()
},
|contents| {
debug!("Successfully read reference file");
if contents.is_empty() {
debug!("Reference file is empty");
} else {
debug!("Reference file contains {} lines", contents.lines().count());
for (i, line) in contents.lines().enumerate() {
if !line.trim().is_empty() {
debug!(" Reference[{}]: {}", i, line.trim());
}
}
}
contents
.lines()
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect()
},
)
},
)
}
/// Output parameters for generating files
struct OutputParams<'a> {
cve_number: &'a str,
git_sha_full: &'a str,
commit_subject: &'a str,
user_name: &'a str,
user_email: &'a str,
script_name: &'a str,
script_version: &'a str,
commit_text: &'a str,
affected_files: &'a Vec<String>,
}
/// Generate and write output files (mbox and/or JSON)
fn generate_output_files(
mbox_path: Option<&Path>,
json_path: Option<&Path>,
params: &OutputParams,
dyad_entries: &[DyadEntry],
additional_references: &[String],
) {
// Generate mbox file if requested
if let Some(path) = mbox_path {
// Create MboxParams from OutputParams
let mbox_params = MboxParams {
cve_number: params.cve_number,
git_sha_full: params.git_sha_full,
commit_subject: params.commit_subject,
user_name: params.user_name,
user_email: params.user_email,
dyad_entries,
script_name: params.script_name,
script_version: params.script_version,
additional_references,
commit_text: params.commit_text,
affected_files: params.affected_files,
};
let mbox_content = generate_mbox(&mbox_params);
if let Err(err) = std::fs::write(path, mbox_content) {
error!("Warning: Failed to write mbox file to {}: {err}", path.display());
} else {
debug!("Wrote mbox file to {path}", path = path.display());
}
}
// Generate JSON file if requested
if let Some(path) = json_path {
// Create CveRecordParams from OutputParams
let json_params = CveRecordParams {
cve_number: params.cve_number,
git_sha_full: params.git_sha_full,
commit_subject: params.commit_subject,
user_name: params.user_name,
user_email: params.user_email,
dyad_entries: dyad_entries.to_vec(),
script_name: params.script_name,
script_version: params.script_version,
additional_references,
commit_text: params.commit_text,
affected_files: params.affected_files,
};
match generate_json(&json_params) {
Ok(json_record) => {
if let Err(err) = std::fs::write(path, json_record) {
error!("Warning: Failed to write JSON file to {}: {err}", path.display());
} else {
debug!("Wrote JSON file to {path}", path = path.display());
}
}
Err(err) => {
error!("Error: Failed to generate JSON record: {err}");
}
}
}
}
/// Main function
fn main() -> Result<()> {
// Parse command line arguments
let args = Args::parse();
// Initialize logging system
initialize_logging(args.verbose);
// Log command line arguments in verbose mode
log_command_args();
// Validate arguments and environment variables
let (cve_number, user_name, user_email, git_shas, vulnerable_shas) =
validate_args_and_env(&args);
// Get required directories and script information
let (kernel_tree, script_dir, script_name, script_version) = get_directories()?;
// Get Git commit information
let (git_sha_full, commit_subject, raw_commit_text, affected_files) = get_commit_info(&kernel_tree, &git_shas)?;
// Process commit text
let commit_text = process_commit_text(&script_dir, &raw_commit_text, args.message.as_deref());
// Run dyad and parse its output
let dyad_entries = run_dyad_and_parse(&script_dir, &git_shas, &vulnerable_shas);
// ***POLICY***
// Determine if we actually have any "pairs" of commits that are in a released kernel such that
// we should be issuing a CVE or not. This is a policy decision from cve.org which requires
// that an actual release is vulnerable in order for a CVE to be issued, it's not just the fact
// that we created and fixed a bug in a git range.
let mut is_vulnerable = false;
for entry in &dyad_entries {
// Skip entries where the vulnerability is in the same version it was fixed
if entry.vulnerable.version() == entry.fixed.version() {
continue;
}
is_vulnerable = true;
}
if !is_vulnerable {
error!("Despite having some vulnerable:fixed kernels, none were in an actual release, so aborting and not assigning a CVE to {git_sha_full}");
std::process::exit(1);
}
// Read additional references from file if specified
let additional_references = read_additional_references(args.reference);
// Create output parameters
let output_params = OutputParams {
cve_number: &cve_number,
git_sha_full: &git_sha_full,
commit_subject: &commit_subject,
user_name: &user_name,
user_email: &user_email,
script_name: &script_name,
script_version: &script_version,
commit_text: &commit_text,
affected_files: &affected_files,
};
// Generate and write output files
generate_output_files(
args.mbox.as_deref(),
args.json.as_deref(),
&output_params,
&dyad_entries,
&additional_references,
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cve_utils::version_utils::{version_is_mainline, version_is_queue, version_is_rc};
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_version_is_rc() {
assert!(version_is_rc("5.16-rc1"));
assert!(version_is_rc("6.1-rc2"));
assert!(!version_is_rc("5.15.1"));
assert!(!version_is_rc("6.0"));
}
#[test]
fn test_version_is_queue() {
assert!(version_is_queue("5.15-queue"));
assert!(!version_is_queue("5.15.1"));
assert!(!version_is_queue("6.0"));
}
#[test]
fn test_version_is_mainline() {
assert!(version_is_mainline("2.6.39"));
assert!(version_is_mainline("5.16-rc1"));
assert!(version_is_mainline("6.0"));
assert!(!version_is_mainline("5.15-queue"));
assert!(!version_is_mainline("5.15.1"));
}
#[test]
fn test_dyad_entry_parsing() {
let entry = DyadEntry::from_str("5.15:11c52d250b34a0862edc29db03fbec23b30db6da:5.16:2b503c8598d1b232e7fc7526bce9326d92331541").unwrap();
assert_eq!(entry.vulnerable.version(), "5.15");
assert_eq!(
entry.vulnerable.git_id(),
"11c52d250b34a0862edc29db03fbec23b30db6da"
);
assert_eq!(entry.fixed.version(), "5.16");
assert_eq!(
entry.fixed.git_id(),
"2b503c8598d1b232e7fc7526bce9326d92331541"
);
assert!(entry.is_fixed());
assert!(entry.is_cross_version());
// Test with a vulnerability that isn't fixed
let entry =
DyadEntry::from_str("5.15:11c52d250b34a0862edc29db03fbec23b30db6da:0:0").unwrap();
assert_eq!(entry.vulnerable.version(), "5.15");
assert_eq!(
entry.vulnerable.git_id(),
"11c52d250b34a0862edc29db03fbec23b30db6da"
);
assert!(entry.fixed.is_empty());
assert_eq!(entry.fixed.version(), "0");
assert_eq!(entry.fixed.git_id(), "0");
assert!(!entry.is_fixed());
// Test with an unknown introduction point
let entry =
DyadEntry::from_str("0:0:5.16:2b503c8598d1b232e7fc7526bce9326d92331541").unwrap();
assert!(entry.vulnerable.is_empty());
assert_eq!(entry.vulnerable.version(), "0");
assert_eq!(entry.vulnerable.git_id(), "0");
assert_eq!(entry.fixed.version(), "5.16");
assert_eq!(
entry.fixed.git_id(),
"2b503c8598d1b232e7fc7526bce9326d92331541"
);
assert!(entry.is_fixed());
assert!(!entry.is_cross_version());
}
#[test]
fn test_invalid_dyad_entry() {
let result = DyadEntry::from_str("invalid:format");
assert!(result.is_err());
}
#[test]
fn test_strip_commit_text() {
let tags = vec![
"Signed-off-by".to_string(),
"Acked-by".to_string(),
"Reviewed-by".to_string(),
];
let commit_text = "Subject: Fix a bug\n\nThis commit fixes a bug in the kernel.\n\nSigned-off-by: Bob <bob@example.com>\nAcked-by: Alice <alice@example.com>\n";
let expected = "In the Linux kernel, the following vulnerability has been resolved:\n\nSubject: Fix a bug\n\nThis commit fixes a bug in the kernel.\n";
let result = strip_commit_text(commit_text, &tags);
assert_eq!(result, expected);
// Test with empty tags
let empty_tags: Vec<String> = Vec::new();
let result = strip_commit_text(commit_text, &empty_tags);
assert_eq!(result, "In the Linux kernel, the following vulnerability has been resolved:\n\nSubject: Fix a bug\n\nThis commit fixes a bug in the kernel.\n\nSigned-off-by: Bob <bob@example.com>\nAcked-by: Alice <alice@example.com>\n");
// Test with multi-paragraph commit
let multi_para_commit = "Subject: Complex fix\n\nParagraph 1 with details.\n\nParagraph 2 with more details.\n\nSigned-off-by: Bob <bob@example.com>\n";
let expected_multi = "In the Linux kernel, the following vulnerability has been resolved:\n\nSubject: Complex fix\n\nParagraph 1 with details.\n\nParagraph 2 with more details.\n";
let result = strip_commit_text(multi_para_commit, &tags);
assert_eq!(result, expected_multi);
}
#[test]
fn test_message_file_functionality() {
use std::fs;
use tempfile::TempDir;
// Create a temporary directory for testing
let temp_dir = TempDir::new().unwrap();
let script_dir = temp_dir.path();
// Create a test .message file
let test_sha = "abc123def456";
let message_file_path = script_dir.join(format!("{}.message", test_sha));
let custom_message = "This is a custom CVE description from a .message file\n\nIt should be used instead of the git commit message.";
fs::write(&message_file_path, custom_message).unwrap();
// Test that read_message_file finds and reads the file
let result = read_message_file(&message_file_path).unwrap();
assert_eq!(result, Some(custom_message.to_string()));
// Test process_commit_text with message file
let commit_text = "Original commit message that should be ignored";
let processed = process_commit_text(script_dir, commit_text, Some(&message_file_path));
assert!(processed.starts_with("In the Linux kernel, the following vulnerability has been resolved:"));
assert!(processed.contains("This is a custom CVE description"));
assert!(!processed.contains("Original commit message"));
// Test process_commit_text without message file
fs::write(script_dir.join("tags"), "Signed-off-by\n").unwrap();
let processed_no_message = process_commit_text(script_dir, commit_text, None);
assert!(processed_no_message.starts_with("In the Linux kernel, the following vulnerability has been resolved:"));
assert!(processed_no_message.contains("Original commit message"));
// Test empty message file - should fall back to commit message
let empty_message_path = script_dir.join("empty.message");
fs::write(&empty_message_path, " \n\n \n").unwrap();
let processed_empty = process_commit_text(script_dir, commit_text, Some(&empty_message_path));
assert!(processed_empty.starts_with("In the Linux kernel, the following vulnerability has been resolved:"));
assert!(processed_empty.contains("Original commit message"));
}
#[test]
fn test_determine_default_status() {
// Test with vulnerable_version = 0
let entries =
vec![DyadEntry::from_str("0:0:5.15:11c52d250b34a0862edc29db03fbec23b30db6da").unwrap()];
assert_eq!(
utils::version::determine_default_status(&entries),
"affected"
);
// Test with invalid git id
/* FIXME, does not build, but you get the idea of what we should be testing...
match DyadEntry::from_str("5.10:abcdef123456:5.15:11c52d250b34a0862edc29db03fbec23b30db6da") {
Ok(d) => {
assert_eq!(0, 0);
}
Err(e) => {
assert_eq!(e, Err(InvalidDyadGitId("abcdef123456")));
}
} */
// Test with mainline vulnerable version that's different from the fixed version
let entries = vec![
DyadEntry::from_str("5.11:e478d6029dca9d8462f426aee0d32896ef64f10f:5.15:11c52d250b34a0862edc29db03fbec23b30db6da").unwrap(),
];
assert_eq!(
utils::version::determine_default_status(&entries),
"affected"
);
// Test with mainline version that's both vulnerable and fixed in the same version
// This should be "unaffected" because no actually released version was affected
let entries = vec![
DyadEntry::from_str("6.1:7bd7ad3c310cd6766f170927381eea0aa6f46c69:6.1:1a0398915d2243fc14be6506a6d226e0593a1c33").unwrap(),
];
assert_eq!(
utils::version::determine_default_status(&entries),
"unaffected"
);
// Test with multiple entries, one with vulnerable_version = 0
let entries = vec![
DyadEntry::from_str("5.11:e478d6029dca9d8462f426aee0d32896ef64f10f:5.15:11c52d250b34a0862edc29db03fbec23b30db6da").unwrap(),
DyadEntry::from_str("0:0:6.1:1a0398915d2243fc14be6506a6d226e0593a1c33").unwrap(),
];
assert_eq!(
utils::version::determine_default_status(&entries),
"affected"
);
// Test with multiple entries, mix of same-version fixes and different-version fixes
let entries = vec![
DyadEntry::from_str("5.15.1:569fd073a954616c8be5a26f37678a1311cc7f91:5.15.2:5dbe126056fb5a1a4de6970ca86e2e567157033a").unwrap(),
DyadEntry::from_str("6.1:7bd7ad3c310cd6766f170927381eea0aa6f46c69:6.1:1a0398915d2243fc14be6506a6d226e0593a1c33").unwrap(),
];
assert_eq!(
utils::version::determine_default_status(&entries),
"unaffected"
);
}
#[test]
fn test_generate_version_ranges() {
// Test with a single entry for a stable kernel
let entries = vec![
DyadEntry::from_str("5.15:11c52d250b34a0862edc29db03fbec23b30db6da:5.16:2b503c8598d1b232e7fc7526bce9326d92331541").unwrap(),
];
let kernel_versions = utils::version::generate_version_ranges(&entries, "unaffected");
let git_versions = utils::version::generate_git_ranges(&entries);
// Check git versions
assert_eq!(git_versions.len(), 1);
assert_eq!(
git_versions[0].version,
"11c52d250b34a0862edc29db03fbec23b30db6da"
);
assert_eq!(
git_versions[0].less_than,
Some("2b503c8598d1b232e7fc7526bce9326d92331541".to_string())
);
assert_eq!(git_versions[0].status, "affected");
// Check kernel versions - expect 2 entries based on the implementation
assert_eq!(kernel_versions.len(), 2);
// First entry: explicit affected version
assert_eq!(kernel_versions[0].version, "5.15");
assert_eq!(kernel_versions[0].status, "affected");
// Second entry: version range
assert_eq!(kernel_versions[1].version, "5.15");
assert_eq!(kernel_versions[1].less_than, Some("5.16".to_string()));
assert_eq!(kernel_versions[1].status, "affected");
// Test with default status "affected"
let entries = vec![
DyadEntry::from_str("6.0:d640c4cb8f2f933c0ca896541f9de7fb1ae245f4:6.1:c1547f12df8b8e9ca2686accee43213ecd117efe").unwrap(),
];
let kernel_versions = utils::version::generate_version_ranges(&entries, "affected");
let git_versions = utils::version::generate_git_ranges(&entries);
// Check git versions
assert_eq!(git_versions.len(), 1);
// Check kernel versions (should include unaffected entries)
assert!(kernel_versions.len() >= 2);
// Find the affected version
let affected = kernel_versions
.iter()
.find(|v| v.status == "affected")
.unwrap();
assert_eq!(affected.version, "6.0");
// Find the unaffected version
let unaffected = kernel_versions
.iter()
.find(|v| v.status == "unaffected" && v.version == "6.1")
.unwrap();
assert_eq!(unaffected.version, "6.1");
// Test with multiple entries
let entries = vec![
DyadEntry::from_str("5.15:11c52d250b34a0862edc29db03fbec23b30db6da:5.16:2b503c8598d1b232e7fc7526bce9326d92331541").unwrap(),
DyadEntry::from_str("6.0:d640c4cb8f2f933c0ca896541f9de7fb1ae245f4:6.1:c1547f12df8b8e9ca2686accee43213ecd117efe").unwrap(),
];
let kernel_versions = utils::version::generate_version_ranges(&entries, "unaffected");
let git_versions = utils::version::generate_git_ranges(&entries);
// Check git versions (should have two entries)
assert_eq!(git_versions.len(), 2);
// Check kernel versions (should have four entries based on implementation)
assert_eq!(kernel_versions.len(), 4);
}
#[test]
fn test_read_tags_file() {
let dir = tempdir().unwrap();
let tags_path = dir.path().join("tags");
// Create a test tags file
let tags_content = "Signed-off-by\nAcked-by\nReviewed-by\n";
let mut file = File::create(&tags_path).unwrap();
file.write_all(tags_content.as_bytes()).unwrap();
// Test reading the tags file
let tags = utils::file::read_tags_file(dir.path()).unwrap();
assert_eq!(tags.len(), 3);
assert_eq!(tags[0], "Signed-off-by");
assert_eq!(tags[1], "Acked-by");
assert_eq!(tags[2], "Reviewed-by");
// Test with empty file
let empty_tags_path = dir.path().join("tags");
std::fs::remove_file(&tags_path).unwrap();
let mut file = File::create(&empty_tags_path).unwrap();
file.write_all(b"").unwrap();
let tags = utils::file::read_tags_file(dir.path()).unwrap();
assert_eq!(tags.len(), 0);
// Test with file containing empty lines and comments
let mixed_content = "Tag1\n\nTag2\n# This is a comment\nTag3\n";
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&empty_tags_path)
.unwrap();
file.write_all(mixed_content.as_bytes()).unwrap();
// Read tags file - our implementation should ignore empty lines and comments
let tags = utils::file::read_tags_file(dir.path()).unwrap();
assert_eq!(tags.len(), 3); // Changed from 4 to 3 to match implementation
}
#[test]
fn test_read_uuid() {
let dir = tempdir().unwrap();
let uuid_path = dir.path().join("linux.uuid");
// Create a test UUID file
let uuid_content = "12345678-abcd-efgh-ijkl-mnopqrstuvwx\n";
let mut file = File::create(&uuid_path).unwrap();
file.write_all(uuid_content.as_bytes()).unwrap();
// Test reading the UUID file
let uuid = utils::file::read_uuid(dir.path()).unwrap();
assert_eq!(uuid, "12345678-abcd-efgh-ijkl-mnopqrstuvwx");
// Test with empty file
std::fs::remove_file(&uuid_path).unwrap();
let empty_path = dir.path().join("linux.uuid");
let mut file = File::create(&empty_path).unwrap();
file.write_all(b"").unwrap();
let result = utils::file::read_uuid(dir.path());
assert!(result.is_err());
}
#[test]
fn test_get_script_version() {
// With the new implementation, script_version is obtained directly from Cargo.toml
// using the env!("CARGO_PKG_VERSION") macro, which is evaluated at compile time
// We can't easily test the exact value since it depends on the Cargo.toml
// But we can verify the format is correct (typically something like "0.1.0")
let version = env!("CARGO_PKG_VERSION");
// Check that it's a valid semver format
assert!(
version.split('.').count() >= 2,
"Version should have at least major.minor format"
);
// Check that it contains only valid semver characters
assert!(
version.chars().all(|c| c.is_ascii_digit() || c == '.'),
"Version should only contain digits and dots"
);
}
}