공개된 소스
소스 ZIP 다운로드
CH-J Tools
특정 애플리케이션 릴리스의 디렉터리와 파일을 살펴보세요.
CH-J / Server Manager
1704 개 파일
0.1.2-beta
추가됨: · 빌드됨:
1
//! Mandatory license acceptance and operational guidance (port of2
//! `legal/licenseAcceptance.js` + `guidanceDocuments.js`). A new version, build,3
//! license text or guidance text always requires a fresh acceptance.5
use crate::core::error::{CoreError, CoreResult};6
use crate::core::storage::atomic_write_json;7
use serde_json::{json, Map, Value};8
use sha2::{Digest, Sha256};9
use std::fs;10
use std::path::{Path, PathBuf};12
pub const LICENSE_NAME: &str = "CH-J Proprietary Software License";13
pub const LICENSE_VERSION: &str = "1.14";14
pub 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
];20
pub struct LicenseAcceptance {21
file: PathBuf,22
pub text: String,23
pub guidance: Map<String, Value>,24
pub identity: Map<String, Value>,25
}27
fn sha256_hex(data: &[u8]) -> String {28
hex::encode(Sha256::digest(data))29
}31
pub 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))44
}46
impl 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_shown84
|| 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
}98
}100
#[cfg(test)]101
mod 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
}140
}SHA-256
de3e91786ecef571d482225b7126592f331850a6328e5401affaa0f05b8db95cCH-J Proprietary Software License 1.14 · 소스는 CH-J Proprietary Software License 1.14에 따라 제공됩니다. 소스를 공개한다고 해서 라이선스 조항이 바뀌거나 추가 권리가 부여되지는 않습니다.
압축 파일 SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f