Skip to main content

rns_git/
util.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use rns_crypto::identity::Identity;
5use rns_crypto::OsRng;
6use rns_net::storage;
7
8use crate::{Error, Result};
9
10pub fn default_rngit_dir() -> PathBuf {
11    std::env::var_os("RNGIT_CONFIG")
12        .map(PathBuf::from)
13        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".rngit")))
14        .unwrap_or_else(|| PathBuf::from(".rngit"))
15}
16
17pub fn default_reticulum_dir() -> Option<PathBuf> {
18    std::env::var_os("RNS_CONFIG").map(PathBuf::from)
19}
20
21pub fn ensure_dir(path: &Path) -> Result<()> {
22    fs::create_dir_all(path)?;
23    Ok(())
24}
25
26pub fn load_or_create_identity(path: &Path) -> Result<Identity> {
27    if path.exists() {
28        return storage::load_identity(path).map_err(Error::from);
29    }
30
31    if let Some(parent) = path.parent() {
32        fs::create_dir_all(parent)?;
33    }
34    let identity = Identity::new(&mut OsRng);
35    storage::save_identity(&identity, path)?;
36    Ok(identity)
37}
38
39pub fn hex(bytes: &[u8]) -> String {
40    const CHARS: &[u8; 16] = b"0123456789abcdef";
41    let mut out = String::with_capacity(bytes.len() * 2);
42    for b in bytes {
43        out.push(CHARS[(b >> 4) as usize] as char);
44        out.push(CHARS[(b & 0x0f) as usize] as char);
45    }
46    out
47}
48
49pub fn parse_hex_16(s: &str) -> Result<[u8; 16]> {
50    let clean = s.trim();
51    if clean.len() != 32 {
52        return Err(Error::msg("expected 32 hex characters"));
53    }
54    let mut out = [0u8; 16];
55    for i in 0..16 {
56        out[i] = parse_hex_byte(&clean[i * 2..i * 2 + 2])?;
57    }
58    Ok(out)
59}
60
61fn parse_hex_byte(s: &str) -> Result<u8> {
62    u8::from_str_radix(s, 16).map_err(|_| Error::msg("invalid hex"))
63}
64
65pub fn parse_rns_url(url: &str) -> Result<([u8; 16], String)> {
66    let rest = url
67        .strip_prefix("rns://")
68        .ok_or_else(|| Error::msg("RNS Git URL must start with rns://"))?;
69    let (hash, repo) = rest
70        .split_once('/')
71        .ok_or_else(|| Error::msg("RNS Git URL must be rns://<destination>/<repo>"))?;
72    let dest_hash = parse_hex_16(hash)?;
73    validate_repo_name(repo)?;
74    Ok((dest_hash, repo.trim_matches('/').to_string()))
75}
76
77pub fn validate_repo_name(name: &str) -> Result<()> {
78    let trimmed = name.trim_matches('/');
79    if trimmed.is_empty() || trimmed.len() > 256 {
80        return Err(Error::msg("invalid repository name"));
81    }
82    for component in trimmed.split('/') {
83        if component.is_empty() || component == "." || component == ".." || component.contains('\\')
84        {
85            return Err(Error::msg("invalid repository name"));
86        }
87    }
88    Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn parse_url_extracts_destination_and_repo() {
97        let (hash, repo) =
98            parse_rns_url("rns://00112233445566778899aabbccddeeff/group/repo").unwrap();
99        assert_eq!(hash[0], 0x00);
100        assert_eq!(hash[15], 0xff);
101        assert_eq!(repo, "group/repo");
102    }
103
104    #[test]
105    fn rejects_path_traversal_repo_names() {
106        assert!(validate_repo_name("../repo").is_err());
107        assert!(validate_repo_name("group/../repo").is_err());
108        assert!(validate_repo_name("group/repo").is_ok());
109    }
110}