diff --git a/Cargo.lock b/Cargo.lock index 015f329..bf4db5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3567,6 +3567,13 @@ dependencies = [ "zbus 4.4.0", ] +[[package]] +name = "shturman-manifest-validator" +version = "0.0.0" +dependencies = [ + "shturman-sdk", +] + [[package]] name = "shturman-power" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index d850df8..1ad6d1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/core/shturman-settings", "crates/core/shturman-power", "crates/apps/shturman-shell", + "crates/tools/shturman-manifest-validator", ] [workspace.package] diff --git a/crates/tools/shturman-manifest-validator/Cargo.toml b/crates/tools/shturman-manifest-validator/Cargo.toml new file mode 100644 index 0000000..63e9866 --- /dev/null +++ b/crates/tools/shturman-manifest-validator/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "shturman-manifest-validator" +version = "0.0.0" +edition.workspace = true +license.workspace = true + +[dependencies] +shturman-sdk = { path = "../../shturman-sdk" } diff --git a/crates/tools/shturman-manifest-validator/src/main.rs b/crates/tools/shturman-manifest-validator/src/main.rs new file mode 100644 index 0000000..4173360 --- /dev/null +++ b/crates/tools/shturman-manifest-validator/src/main.rs @@ -0,0 +1,36 @@ +//! Валидатор манифеста плагина (F02). `shturman-manifest-validator `. + +mod validate; + +use std::process::ExitCode; + +fn main() -> ExitCode { + let Some(path) = std::env::args().nth(1) else { + eprintln!("usage: shturman-manifest-validator "); + return ExitCode::from(2); + }; + let yaml = match std::fs::read_to_string(&path) { + Ok(s) => s, + Err(e) => { + eprintln!("не прочитать {path}: {e}"); + return ExitCode::from(2); + } + }; + match validate::validate_yaml(&yaml) { + Err(e) => { + eprintln!("✗ {e}"); + ExitCode::FAILURE + } + Ok(errs) if errs.is_empty() => { + println!("✓ манифест валиден: {path}"); + ExitCode::SUCCESS + } + Ok(errs) => { + eprintln!("✗ нарушений: {}", errs.len()); + for e in errs { + eprintln!(" - {e}"); + } + ExitCode::FAILURE + } + } +} diff --git a/crates/tools/shturman-manifest-validator/src/validate.rs b/crates/tools/shturman-manifest-validator/src/validate.rs new file mode 100644 index 0000000..e1c6e84 --- /dev/null +++ b/crates/tools/shturman-manifest-validator/src/validate.rs @@ -0,0 +1,117 @@ +//! Правила валидации манифеста поверх схемы (plugin-sdk §2/§3, F §3). Схема — `shturman_sdk::Manifest`. + +use shturman_sdk::Manifest; + +/// Каталог стандартных сигналов (data-model §4). `vehicle_read` допускает только их. +const VEHICLE_SIGNALS: &[&str] = &[ + "speed", + "rpm", + "engine_load", + "coolant_temp", + "intake_temp", + "maf", + "throttle", + "intake_pressure", + "fuel_level", + "module_voltage", + "ambient_temp", + "oil_temp", + "fuel_rate", + "run_time", + "mil_on", + "dtc_count", + "distance_mil", +]; + +/// Поддерживаемые мажоры API (plugin-sdk §7). +const SUPPORTED_API: &[&str] = &["1"]; + +/// Проверить распарсенный манифест. Пустой вектор — валиден. +pub fn validate(m: &Manifest) -> Vec { + let mut errs = Vec::new(); + + // id `ru.shturman.*` закрыт за first-party (F §3) + if m.plugin.id.starts_with("ru.shturman.") { + errs.push(format!( + "id '{}' использует зарезервированный префикс ru.shturman.* (закрыт за first-party)", + m.plugin.id + )); + } + + // shturman_api поддержан + if !SUPPORTED_API.contains(&m.plugin.shturman_api.as_str()) { + errs.push(format!( + "shturman_api '{}' не поддержан (поддерживаются: {SUPPORTED_API:?})", + m.plugin.shturman_api + )); + } + + // квота тайлов: len(tiles) ≤ ui_tiles (plugin-sdk §2) + let tiles = m.extension_points.tiles.len() as u32; + if tiles > m.capabilities.ui_tiles { + errs.push(format!( + "тайлов {tiles} больше квоты ui_tiles {}", + m.capabilities.ui_tiles + )); + } + + // vehicle_read только из каталога data-model + for sig in &m.capabilities.vehicle_read { + if !VEHICLE_SIGNALS.contains(&sig.as_str()) { + errs.push(format!("vehicle_read '{sig}' нет в каталоге data-model")); + } + } + + errs +} + +/// Распарсить YAML (схема) и проверить правила. `Err` — ошибка парсинга; `Ok(errs)` — нарушения правил. +pub fn validate_yaml(yaml: &str) -> Result, String> { + let m = Manifest::from_yaml(yaml).map_err(|e| format!("парсинг манифеста: {e}"))?; + Ok(validate(&m)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture(name: &str) -> String { + let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../fixtures/manifests/"); + std::fs::read_to_string(format!("{dir}{name}")) + .unwrap_or_else(|e| panic!("фикстура {name}: {e}")) + } + + #[test] + fn good_passes() { + assert!(validate_yaml(&fixture("good.yaml")).unwrap().is_empty()); + } + + #[test] + fn reserved_id_rejected() { + let errs = validate_yaml(&fixture("bad-id-reserved.yaml")).unwrap(); + assert!(errs.iter().any(|e| e.contains("ru.shturman"))); + } + + #[test] + fn unsupported_api_rejected() { + let errs = validate_yaml(&fixture("bad-api.yaml")).unwrap(); + assert!(errs.iter().any(|e| e.contains("shturman_api"))); + } + + #[test] + fn tiles_over_quota_rejected() { + let errs = validate_yaml(&fixture("bad-tiles-quota.yaml")).unwrap(); + assert!(errs.iter().any(|e| e.contains("квоты"))); + } + + #[test] + fn unknown_vehicle_read_rejected() { + let errs = validate_yaml(&fixture("bad-vehicle-read.yaml")).unwrap(); + assert!(errs.iter().any(|e| e.contains("vehicle_read"))); + } + + #[test] + fn malformed_yaml_errors() { + assert!(validate_yaml("::: not yaml :::").is_err()); + } +} diff --git a/fixtures/manifests/bad-api.yaml b/fixtures/manifests/bad-api.yaml new file mode 100644 index 0000000..fc709dd --- /dev/null +++ b/fixtures/manifests/bad-api.yaml @@ -0,0 +1,6 @@ +# Неподдерживаемая мажорная версия API (plugin-sdk §7). +plugin: + id: dev.example.future + name: "Из будущего" + version: 0.1.0 + shturman_api: "99" diff --git a/fixtures/manifests/bad-id-reserved.yaml b/fixtures/manifests/bad-id-reserved.yaml new file mode 100644 index 0000000..4a5245d --- /dev/null +++ b/fixtures/manifests/bad-id-reserved.yaml @@ -0,0 +1,6 @@ +# id в зарезервированном префиксе ru.shturman.* — закрыт за first-party (F §3). +plugin: + id: ru.shturman.evil + name: "Подделка" + version: 0.1.0 + shturman_api: "1" diff --git a/fixtures/manifests/bad-tiles-quota.yaml b/fixtures/manifests/bad-tiles-quota.yaml new file mode 100644 index 0000000..b9f2f32 --- /dev/null +++ b/fixtures/manifests/bad-tiles-quota.yaml @@ -0,0 +1,14 @@ +# Тайлов больше, чем квота ui_tiles (plugin-sdk §2). +plugin: + id: dev.example.greedy + name: "Жадный" + version: 0.1.0 + shturman_api: "1" +capabilities: + ui_tiles: 1 +extension_points: + tiles: + - id: a + title: "A" + - id: b + title: "B" diff --git a/fixtures/manifests/bad-vehicle-read.yaml b/fixtures/manifests/bad-vehicle-read.yaml new file mode 100644 index 0000000..d5710ff --- /dev/null +++ b/fixtures/manifests/bad-vehicle-read.yaml @@ -0,0 +1,8 @@ +# vehicle_read с сигналом вне каталога data-model. +plugin: + id: dev.example.bogus + name: "Выдумщик" + version: 0.1.0 + shturman_api: "1" +capabilities: + vehicle_read: [rocket_boost] diff --git a/fixtures/manifests/good.yaml b/fixtures/manifests/good.yaml new file mode 100644 index 0000000..296a6b3 --- /dev/null +++ b/fixtures/manifests/good.yaml @@ -0,0 +1,12 @@ +plugin: + id: dev.example.fuel-tracker + name: "Учёт расхода" + version: 0.1.0 + shturman_api: "1" +capabilities: + vehicle_read: [speed, maf, fuel_level, fuel_rate] + ui_tiles: 1 +extension_points: + tiles: + - id: consumption + title: "Расход"