CH-J Toolsspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Tools

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / Server Manager 872 souborů
core-20261030T220507Z-61e4558d889f Přidáno: · Sestaveno:
legal.rs Přidáno: Datum není evidováno 6,5 KB · 140 řádků
Kód Stáhnout soubor
1//! Mandatory license acceptance and operational guidance (port of
2//! `legal/licenseAcceptance.js` + `guidanceDocuments.js`). A new version, build,
3//! license text or guidance text always requires a fresh acceptance.
5use crate::core::error::{CoreError, CoreResult};
6use crate::core::storage::atomic_write_json;
7use serde_json::{json, Map, Value};
8use sha2::{Digest, Sha256};
9use std::fs;
10use std::path::{Path, PathBuf};
12pub const LICENSE_NAME: &str = "CH-J Proprietary Software License";
13pub const LICENSE_VERSION: &str = "1.14";
14pub const GUIDANCE_FILES: [(&str, &str); 3] = [
15 ("cs", "provozni-bezpecnost-a-zalohovani.md"),
16 ("de", "betriebssicherheit-und-datensicherung.md"),
17 ("en", "operational-safety-and-backup-guidance.md"),
18];
20pub struct LicenseAcceptance {
21 file: PathBuf,
22 pub text: String,
23 pub guidance: Map<String, Value>,
24 pub identity: Map<String, Value>,
27fn sha256_hex(data: &[u8]) -> String {
28 hex::encode(Sha256::digest(data))
31pub fn load_guidance(docs_root: &Path) -> CoreResult<(Map<String, Value>, String)> {
32 let mut documents = Map::new();
33 for (language, file) in GUIDANCE_FILES {
34 let text = fs::read_to_string(docs_root.join(file))
35 .map_err(|_| CoreError::plain(format!("Missing or incomplete operational guidance: {file}")))?;
36 if text.encode_utf16().count() < 1000 {
37 return Err(CoreError::plain(format!("Missing or incomplete operational guidance: {file}")));
38 }
39 documents.insert(language.to_string(), Value::String(text));
40 }
41 // Same digest as 0.1.0: SHA-256 of JSON.stringify({cs, de, en}).
42 let digest = sha256_hex(serde_json::to_string(&Value::Object(documents.clone()))?.as_bytes());
43 Ok((documents, digest))
46impl LicenseAcceptance {
47 pub fn new(storage_root: &Path, license_path: &Path, docs_root: &Path, app_version: &str, build_id: &str) -> CoreResult<Self> {
48 let text = fs::read_to_string(license_path).map_err(|_| CoreError::plain("The complete CH-J application license 1.14 is required."))?;
49 let header_ok = regex::Regex::new(r"^CH-J\s+Proprietary Software License\s*\n\s*Version 1\.14\b")
50 .map(|pattern| pattern.is_match(&text))
51 .unwrap_or(false);
52 if !header_ok || text.encode_utf16().count() < 10000 || !text.trim_end().ends_with("End of License.") {
53 return Err(CoreError::plain("The complete CH-J application license 1.14 is required."));
54 }
55 let (guidance, guidance_sha) = load_guidance(docs_root)?;
56 let mut identity = Map::new();
57 identity.insert("license".into(), json!(LICENSE_NAME));
58 identity.insert("licenseVersion".into(), json!(LICENSE_VERSION));
59 identity.insert("sha256".into(), json!(sha256_hex(text.as_bytes())));
60 identity.insert("appVersion".into(), json!(app_version));
61 identity.insert("buildId".into(), json!(build_id));
62 identity.insert("guidanceSha256".into(), json!(guidance_sha));
63 Ok(Self { file: storage_root.join("legal").join("license-acceptance.json"), text, guidance, identity })
64 }
66 pub fn identity_value(&self, key: &str) -> &str {
67 self.identity.get(key).and_then(Value::as_str).unwrap_or("")
68 }
70 pub fn is_accepted(&self) -> bool {
71 let Ok(raw) = fs::read_to_string(&self.file) else { return false };
72 let Ok(record) = serde_json::from_str::<Value>(&raw) else { return false };
73 record.get("schemaVersion").and_then(Value::as_f64) == Some(1.0)
74 && record.get("accepted") == Some(&Value::Bool(true))
75 && record.get("guidanceDisplayed") == Some(&Value::Bool(true))
76 && record.get("acceptedAt").and_then(Value::as_str).map(|s| chrono::DateTime::parse_from_rfc3339(s).is_ok()).unwrap_or(false)
77 && self.identity.iter().all(|(key, value)| record.get(key) == Some(value))
78 }
80 pub fn accept(&self, payload: &Value, guidance_shown: bool) -> CoreResult<()> {
81 if payload.get("accepted") != Some(&Value::Bool(true))
82 || payload.get("sha256").and_then(Value::as_str) != Some(self.identity_value("sha256"))
83 || !guidance_shown
84 || payload.get("guidanceSha256").and_then(Value::as_str) != Some(self.identity_value("guidanceSha256"))
85 {
86 return Err(CoreError::plain("Explicit acceptance of the displayed license is required."));
87 }
88 let mut record = Map::new();
89 record.insert("schemaVersion".into(), json!(1));
90 for (key, value) in &self.identity {
91 record.insert(key.clone(), value.clone());
92 }
93 record.insert("accepted".into(), json!(true));
94 record.insert("guidanceDisplayed".into(), json!(true));
95 record.insert("acceptedAt".into(), json!(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)));
96 atomic_write_json(&self.file, &Value::Object(record))
97 }
100#[cfg(test)]
101mod tests {
102 use super::*;
104 fn fixture(dir: &Path) -> (PathBuf, PathBuf) {
105 let license = dir.join("LICENSE");
106 let mut text = String::from("CH-J Proprietary Software License\nVersion 1.14\n\n");
107 text.push_str(&"x".repeat(10050));
108 text.push_str("\nEnd of License.\n");
109 fs::write(&license, text).unwrap();
110 let docs = dir.join("docs");
111 fs::create_dir_all(&docs).unwrap();
112 for (_, file) in GUIDANCE_FILES {
113 fs::write(docs.join(file), "g".repeat(1200)).unwrap();
114 }
115 (license, docs)
116 }
118 #[test]
119 fn acceptance_is_bound_to_identity() {
120 let dir = tempfile::tempdir().unwrap();
121 let (license, docs) = fixture(dir.path());
122 let acceptance = LicenseAcceptance::new(dir.path(), &license, &docs, "0.1.1", "build-a").unwrap();
123 assert!(!acceptance.is_accepted());
124 let payload = json!({"accepted": true, "sha256": acceptance.identity_value("sha256"), "guidanceSha256": acceptance.identity_value("guidanceSha256")});
125 assert!(acceptance.accept(&payload, false).is_err());
126 acceptance.accept(&payload, true).unwrap();
127 assert!(acceptance.is_accepted());
128 let next_build = LicenseAcceptance::new(dir.path(), &license, &docs, "0.1.1", "build-b").unwrap();
129 assert!(!next_build.is_accepted());
130 }
132 #[test]
133 fn incomplete_license_is_rejected() {
134 let dir = tempfile::tempdir().unwrap();
135 let (_, docs) = fixture(dir.path());
136 let short = dir.path().join("SHORT");
137 fs::write(&short, "CH-J Proprietary Software License\nVersion 1.14\nEnd of License.").unwrap();
138 assert!(LicenseAcceptance::new(dir.path(), &short, &docs, "0.1.1", "b").is_err());
139 }
SHA-256de3e91786ecef571d482225b7126592f331850a6328e5401affaa0f05b8db95c

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df
Soubory