initial commit

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ife1391ed23a1e7f388b1b5eca90b9ea76a6a6964
This commit is contained in:
raf 2026-01-29 19:36:25 +03:00
commit ef28bdaeb4
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
63 changed files with 17292 additions and 0 deletions

231
src/utils/hash.rs Normal file
View 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
View 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
View 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)
}