Skip to main content

str_format/
util.rs

1//! 通用工具:UUID v7、SHA-256、时间与命名判定。
2
3use sha2::{Digest, Sha256};
4use std::path::{Component, Path};
5
6/// 元数据文件名。
7pub const META_FILE: &str = "._meta";
8/// bundle 级 Schema 目录。
9pub const SCHEMA_DIR: &str = "._schema";
10/// 派生缓存目录(可删、建议 gitignore)。
11pub const CACHE_DIR: &str = "._cache";
12/// 短期写入锁文件名。
13pub const LOCK_FILE: &str = ".lock";
14
15/// 生成 UUID v7(时间有序)。
16pub fn new_uuid_v7() -> String {
17    uuid::Uuid::now_v7().to_string()
18}
19
20/// 按 `policies.id_version` 生成新 id:`4` → UUIDv4,其余(含 `7`)→ UUIDv7。
21///
22/// 目录名必须满足 `E_ID_VERSION`,因此生成端必须服从同一条策略。
23pub fn new_uuid(version: usize) -> String {
24    match version {
25        4 => uuid::Uuid::new_v4().to_string(),
26        _ => new_uuid_v7(),
27    }
28}
29
30/// 解析 UUID,返回版本号。
31pub fn uuid_version(s: &str) -> Option<usize> {
32    uuid::Uuid::parse_str(s).ok().map(|u| u.get_version_num())
33}
34
35/// 是否为合法 UUID 字面量。
36pub fn is_uuid(s: &str) -> bool {
37    uuid::Uuid::parse_str(s).is_ok()
38}
39
40/// 字节数组 → 小写十六进制。
41pub fn hex(bytes: &[u8]) -> String {
42    const TABLE: &[u8; 16] = b"0123456789abcdef";
43    let mut out = String::with_capacity(bytes.len() * 2);
44    for b in bytes {
45        out.push(TABLE[(b >> 4) as usize] as char);
46        out.push(TABLE[(b & 0x0f) as usize] as char);
47    }
48    out
49}
50
51/// 计算字节串 SHA-256。
52pub fn sha256_bytes(data: &[u8]) -> String {
53    let mut h = Sha256::new();
54    h.update(data);
55    hex(&h.finalize())
56}
57
58/// 计算文件 SHA-256。
59pub fn sha256_file(path: &Path) -> std::io::Result<String> {
60    let mut file = std::fs::File::open(path)?;
61    let mut h = Sha256::new();
62    std::io::copy(&mut file, &mut h)?;
63    Ok(hex(&h.finalize()))
64}
65
66/// 当前 UTC 时间,RFC3339 形式(统一 `+00:00` 偏移)。
67pub fn now_rfc3339() -> String {
68    let now = time::OffsetDateTime::now_utc();
69    format!(
70        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}+00:00",
71        now.year(),
72        u8::from(now.month()),
73        now.day(),
74        now.hour(),
75        now.minute(),
76        now.second()
77    )
78}
79
80/// 解析 RFC3339(offset date-time)。
81pub fn parse_rfc3339(s: &str) -> Option<time::OffsetDateTime> {
82    time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339).ok()
83}
84
85/// `dir` 相对 `root` 的深度(ROOT 自身为 0)。
86pub fn relative_depth(root: &Path, dir: &Path) -> usize {
87    dir.strip_prefix(root)
88        .map(|rel| {
89            rel.components()
90                .filter(|c| matches!(c, Component::Normal(_)))
91                .count()
92        })
93        .unwrap_or(0)
94}
95
96/// 是否为格式保留名(以 `._` 开头)。
97pub fn is_reserved_name(name: &str) -> bool {
98    name.starts_with("._")
99}
100
101/// 是否为 `._meta`。
102pub fn is_meta_file(name: &str) -> bool {
103    name == META_FILE
104}
105
106/// 是否为 `.lock`。
107pub fn is_lock_file(name: &str) -> bool {
108    name == LOCK_FILE
109}
110
111/// 是否为操作系统 / 工具元数据:一律豁免,不参与校验(规范 3.4)。
112///
113/// - `._*` 形式的**普通文件**是 macOS AppleDouble 伴生文件(`._meta` 本身不是噪声);
114/// - `.git` / `.gitignore` / `.hg` / `.svn` 等是版本控制元数据 —— 真实项目必然存在;
115/// - `.github/` 是代码托管平台的元数据:GitHub Actions 的工作流**必须**位于
116///   `.github/workflows/`(路径不可改名),同属「工具元数据」,故一并豁免。
117pub fn is_os_noise(name: &str) -> bool {
118    matches!(
119        name,
120        ".DS_Store"
121            | "Thumbs.db"
122            | "desktop.ini"
123            | ".git"
124            | ".gitignore"
125            | ".gitattributes"
126            | ".gitmodules"
127            | ".gitkeep"
128            | ".github"
129            | ".hg"
130            | ".hgignore"
131            | ".svn"
132            | ".jj"
133    ) || (name.starts_with("._") && name != META_FILE)
134}
135
136/// 是否为**独立子 bundle**:目录名以 `.str` 结尾。
137///
138/// `.str` 目录是 bundle 的**硬边界**:父 bundle 不进入、不把它当作分支
139/// (同 `.app` 嵌套语义)。父级 `entries` 中应表达为 `role = "bundle"`。
140pub fn is_sub_bundle(name: &str) -> bool {
141    name.ends_with(".str")
142}
143
144/// 是否为其它点文件(非 `._meta` / `.lock`)→ `W_DOTFILE`。
145pub fn is_other_dotfile(name: &str) -> bool {
146    name.starts_with('.') && !is_meta_file(name) && !is_lock_file(name)
147}
148
149/// 便捷:路径显示为 bundle 内相对路径。
150pub fn rel_display(root: &Path, path: &Path) -> String {
151    let rel = path.strip_prefix(root).unwrap_or(path);
152    let s = rel.display().to_string();
153    if s.is_empty() { ".".to_string() } else { s }
154}