initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ife1391ed23a1e7f388b1b5eca90b9ea76a6a6964
This commit is contained in:
commit
ef28bdaeb4
63 changed files with 17292 additions and 0 deletions
231
src/utils/hash.rs
Normal file
231
src/utils/hash.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use md5::{Digest as Md5Digest, Md5};
|
||||
use sha1::Sha1;
|
||||
use sha2::{Sha256, Sha512};
|
||||
|
||||
use crate::error::{PakkerError, Result};
|
||||
|
||||
/// Compute Murmur2 hash (32-bit) for `CurseForge` fingerprinting
|
||||
#[allow(dead_code)]
|
||||
pub fn compute_murmur2_hash(data: &[u8]) -> u32 {
|
||||
murmur2_hash(data, 1)
|
||||
}
|
||||
|
||||
/// Murmur2 hash implementation
|
||||
#[allow(dead_code)]
|
||||
fn murmur2_hash(data: &[u8], seed: u32) -> u32 {
|
||||
const M: u32 = 0x5BD1E995;
|
||||
const R: i32 = 24;
|
||||
|
||||
let mut h: u32 = seed ^ (data.len() as u32);
|
||||
let mut chunks = data.chunks_exact(4);
|
||||
|
||||
for chunk in chunks.by_ref() {
|
||||
let mut k = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
k = k.wrapping_mul(M);
|
||||
k ^= k >> R;
|
||||
k = k.wrapping_mul(M);
|
||||
|
||||
h = h.wrapping_mul(M);
|
||||
h ^= k;
|
||||
}
|
||||
|
||||
let remainder = chunks.remainder();
|
||||
match remainder.len() {
|
||||
3 => {
|
||||
h ^= u32::from(remainder[2]) << 16;
|
||||
h ^= u32::from(remainder[1]) << 8;
|
||||
h ^= u32::from(remainder[0]);
|
||||
h = h.wrapping_mul(M);
|
||||
},
|
||||
2 => {
|
||||
h ^= u32::from(remainder[1]) << 8;
|
||||
h ^= u32::from(remainder[0]);
|
||||
h = h.wrapping_mul(M);
|
||||
},
|
||||
1 => {
|
||||
h ^= u32::from(remainder[0]);
|
||||
h = h.wrapping_mul(M);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
h ^= h >> 13;
|
||||
h = h.wrapping_mul(M);
|
||||
h ^= h >> 15;
|
||||
|
||||
h
|
||||
}
|
||||
|
||||
/// Compute SHA1 hash of a file
|
||||
pub fn compute_sha1<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut hasher = Sha1::new();
|
||||
let mut buffer = [0; 8192];
|
||||
|
||||
loop {
|
||||
let n = reader.read(&mut buffer)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..n]);
|
||||
}
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
/// Compute SHA256 hash of a file
|
||||
pub fn compute_sha256<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0; 8192];
|
||||
|
||||
loop {
|
||||
let n = reader.read(&mut buffer)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..n]);
|
||||
}
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
/// Compute SHA256 hash of byte data
|
||||
pub fn compute_sha256_bytes(data: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
/// Compute SHA512 hash of a file
|
||||
pub fn compute_sha512<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut hasher = Sha512::new();
|
||||
let mut buffer = [0; 8192];
|
||||
|
||||
loop {
|
||||
let n = reader.read(&mut buffer)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..n]);
|
||||
}
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
/// Compute MD5 hash of a file
|
||||
pub fn compute_md5<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut hasher = Md5::new();
|
||||
let mut buffer = [0; 8192];
|
||||
|
||||
loop {
|
||||
let n = reader.read(&mut buffer)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..n]);
|
||||
}
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
/// Verify a file's hash against expected value
|
||||
pub fn verify_hash<P: AsRef<Path>>(
|
||||
path: P,
|
||||
algorithm: &str,
|
||||
expected: &str,
|
||||
) -> Result<bool> {
|
||||
let path = path.as_ref();
|
||||
let actual = match algorithm {
|
||||
"sha1" => compute_sha1(path)?,
|
||||
"sha256" => compute_sha256(path)?,
|
||||
"sha512" => compute_sha512(path)?,
|
||||
"md5" => compute_md5(path)?,
|
||||
_ => {
|
||||
return Err(PakkerError::InternalError(format!(
|
||||
"Unknown hash algorithm: {algorithm}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
|
||||
Ok(actual.eq_ignore_ascii_case(expected))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_murmur2_hash_deterministic() {
|
||||
let data = b"hello world";
|
||||
let hash1 = compute_murmur2_hash(data);
|
||||
let hash2 = compute_murmur2_hash(data);
|
||||
assert_eq!(hash1, hash2, "Murmur2 hash must be deterministic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_murmur2_hash_empty() {
|
||||
let data = b"";
|
||||
let hash = compute_murmur2_hash(data);
|
||||
assert_ne!(hash, 0, "Empty data should produce a non-zero hash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_murmur2_hash_different_inputs() {
|
||||
let hash1 = compute_murmur2_hash(b"hello");
|
||||
let hash2 = compute_murmur2_hash(b"world");
|
||||
assert_ne!(
|
||||
hash1, hash2,
|
||||
"Different inputs should produce different hashes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha256_bytes_deterministic() {
|
||||
let data = b"test data";
|
||||
let hash1 = compute_sha256_bytes(data);
|
||||
let hash2 = compute_sha256_bytes(data);
|
||||
assert_eq!(hash1, hash2, "SHA256 must be deterministic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha256_bytes_format() {
|
||||
let data = b"hello";
|
||||
let hash = compute_sha256_bytes(data);
|
||||
assert_eq!(hash.len(), 64, "SHA256 hex should be 64 characters");
|
||||
assert!(
|
||||
hash.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
"SHA256 should only contain hex digits"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha256_bytes_empty() {
|
||||
let hash = compute_sha256_bytes(b"");
|
||||
assert_eq!(
|
||||
hash,
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha256_bytes_known_value() {
|
||||
// SHA256 of "hello" in hex
|
||||
let expected =
|
||||
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
|
||||
let hash = compute_sha256_bytes(b"hello");
|
||||
assert_eq!(hash, expected);
|
||||
}
|
||||
}
|
||||
35
src/utils/id.rs
Normal file
35
src/utils/id.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use rand::Rng;
|
||||
|
||||
const CHARSET: &[u8] =
|
||||
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const ID_LENGTH: usize = 16;
|
||||
|
||||
/// Generate a random 16-character alphanumeric pakku ID
|
||||
pub fn generate_pakku_id() -> String {
|
||||
let mut rng = rand::rng();
|
||||
(0..ID_LENGTH)
|
||||
.map(|_| {
|
||||
let idx = rng.random_range(0..CHARSET.len());
|
||||
CHARSET[idx] as char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_generate_pakku_id() {
|
||||
let id = generate_pakku_id();
|
||||
assert_eq!(id.len(), ID_LENGTH);
|
||||
assert!(id.chars().all(|c| c.is_alphanumeric()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_ids() {
|
||||
let id1 = generate_pakku_id();
|
||||
let id2 = generate_pakku_id();
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
}
|
||||
56
src/utils/prompt.rs
Normal file
56
src/utils/prompt.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use std::io::{self, Write};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn prompt_user(message: &str) -> Result<String> {
|
||||
print!("{message}");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn prompt_select(message: &str, options: &[String]) -> Result<usize> {
|
||||
println!("{message}");
|
||||
for (i, option) in options.iter().enumerate() {
|
||||
println!(" {}. {}", i + 1, option);
|
||||
}
|
||||
|
||||
loop {
|
||||
print!("Select (1-{}): ", options.len());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
if let Ok(choice) = input.trim().parse::<usize>()
|
||||
&& choice > 0
|
||||
&& choice <= options.len()
|
||||
{
|
||||
return Ok(choice - 1);
|
||||
}
|
||||
|
||||
println!("Invalid selection. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn prompt_confirm(message: &str) -> Result<bool> {
|
||||
print!("{message} (y/n): ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
let answer = input.trim().to_lowercase();
|
||||
Ok(answer == "y" || answer == "yes")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn confirm(message: &str) -> Result<bool> {
|
||||
prompt_confirm(message)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue