use std::fs;
use std::io::{self, Write};
use std::path::Path;
use crate::config::RailConfig;
use crate::error::RailResult;
#[inline]
pub fn fnv1a64(bytes: &[u8]) -> u64 {
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET_BASIS;
for byte in bytes {
hash ^= *byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
pub fn file_fingerprint(path: &Path) -> String {
match fs::read(path) {
Ok(bytes) => format!("fnv1a64:{:016x}", fnv1a64(&bytes)),
Err(_) => "none".to_string(),
}
}
pub fn config_fingerprint(workspace_root: &Path) -> String {
RailConfig::find_config_path(workspace_root)
.map(|p| file_fingerprint(&p))
.unwrap_or_else(|| "none".to_string())
}
pub fn toolchain_fingerprint(workspace_root: &Path) -> String {
["rust-toolchain.toml", "rust-toolchain"]
.iter()
.map(|name| workspace_root.join(name))
.find(|p| p.exists())
.map(|p| file_fingerprint(&p))
.unwrap_or_else(|| "none".to_string())
}
pub fn is_local_path(path: &str) -> bool {
let p = Path::new(path);
if path.starts_with("./") || path.starts_with("../") {
return true;
}
if path.len() >= 3 {
let bytes = path.as_bytes();
if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' && (bytes[2] == b'\\' || bytes[2] == b'/') {
return true;
}
}
if path.starts_with("\\\\") {
return true;
}
if path.starts_with('/') {
if !path.contains("://") && !path.contains('@') {
return true;
}
}
if p.is_absolute() {
return true;
}
if path.contains("://") {
return false;
}
if path.contains('@') {
return false;
}
false
}
pub fn prompt_for_confirmation(message: &str) -> RailResult<bool> {
print!("\n{}: ", message);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().is_empty())
}
pub fn detect_crate_changelog(crate_dir: &cargo_metadata::camino::Utf8Path) -> Option<std::path::PathBuf> {
let changelog_patterns = [
"CHANGELOG.md",
"CHANGELOG.txt",
"CHANGELOG",
"Changelog.md",
"changelog.md",
"CHANGES.md",
"CHANGES.txt",
"CHANGES",
"Changes.md",
"changes.md",
];
for pattern in &changelog_patterns {
let changelog = crate_dir.join(pattern);
if changelog.exists() {
return Some(std::path::PathBuf::from(pattern));
}
}
None
}
pub fn path_to_git_format(path: &Path) -> String {
#[cfg(target_os = "windows")]
{
path.to_string_lossy().replace('\\', "/")
}
#[cfg(not(target_os = "windows"))]
{
path.to_string_lossy().to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_fnv1a64_empty() {
assert_eq!(fnv1a64(b""), 0xcbf29ce484222325);
}
#[test]
fn test_fnv1a64_known_values() {
assert_eq!(fnv1a64(b"a"), 0xaf63dc4c8601ec8c);
assert_eq!(fnv1a64(b"foobar"), 0x85944171f73967e8);
}
#[test]
fn test_fnv1a64_deterministic() {
let data = b"hello world";
assert_eq!(fnv1a64(data), fnv1a64(data));
}
#[test]
fn test_file_fingerprint_missing_file() {
let result = file_fingerprint(Path::new("/nonexistent/path/to/file"));
assert_eq!(result, "none");
}
#[test]
fn test_is_local_path_classification() {
assert!(is_local_path("/home/user/repo"));
assert!(is_local_path("C:\\Users\\test\\repo"));
assert!(is_local_path("C:/Users/test/repo"));
assert!(is_local_path("./repo"));
assert!(is_local_path("../path/to/repo"));
#[cfg(target_os = "windows")]
assert!(is_local_path("\\\\server\\share\\repo"));
assert!(!is_local_path("git@github.com:user/repo.git"));
assert!(!is_local_path("https://github.com/user/repo.git"));
assert!(!is_local_path("ssh://git@github.com/user/repo.git"));
assert!(!is_local_path("repo"));
assert!(!is_local_path(""));
}
#[test]
fn test_path_to_git_format_unix() {
#[cfg(not(target_os = "windows"))]
{
let path = PathBuf::from("/home/user/repo/src/main.rs");
assert_eq!(path_to_git_format(&path), "/home/user/repo/src/main.rs");
let path = PathBuf::from("./relative/path.rs");
assert_eq!(path_to_git_format(&path), "./relative/path.rs");
}
}
#[test]
fn test_path_to_git_format_windows() {
#[cfg(target_os = "windows")]
{
let path = PathBuf::from("C:\\Users\\test\\repo\\src\\main.rs");
assert_eq!(path_to_git_format(&path), "C:/Users/test/repo/src/main.rs");
let path = PathBuf::from("..\\relative\\path.rs");
assert_eq!(path_to_git_format(&path), "../relative/path.rs");
}
}
}