Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 91 additions & 22 deletions common/verify/src/ipk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,23 @@ pub enum DetectionResult {
detection: WebAppDetection,
/// The firmware's web engine (for rendering the compat column).
engine: Option<WebEngine>,
/// Whether the firmware's engine supports the app's ES level.
/// Whether the firmware's engine supports the app's ES **syntax** level
/// (gating — syntax can't be polyfilled).
es: CompatVerdict,
/// Whether the firmware's engine natively provides the runtime APIs the
/// app uses (advisory — APIs can be polyfilled).
api: CompatVerdict,
},
Service {
detection: ServiceRuntimeDetection,
/// The firmware's Node.js version — informational only. There is no
/// compat verdict for services: `engines.node` isn't trusted and webOS
/// services carry no other reliable runtime requirement.
/// The firmware's Node.js version.
available_node: Option<Version>,
/// Whether the firmware's Node.js supports the service's ES **syntax**
/// level (gating — derived from the code, not `engines.node`).
node: CompatVerdict,
/// Whether the firmware's Node.js natively provides the runtime APIs the
/// service uses (advisory).
api: CompatVerdict,
},
}

Expand All @@ -60,19 +68,27 @@ pub enum CompatVerdict {
}

impl DetectionResult {
/// The compatibility verdict for this component on this firmware, if it has
/// one. Services are informational only (no verdict).
pub fn verdict(&self) -> Option<&CompatVerdict> {
/// The gating compatibility verdict for this component on this firmware
/// (web app: ES syntax vs engine; service: ES syntax vs Node.js).
pub fn verdict(&self) -> &CompatVerdict {
match self {
DetectionResult::WebApp { es, .. } => Some(es),
DetectionResult::Service { .. } => None,
DetectionResult::WebApp { es, .. } => es,
DetectionResult::Service { node, .. } => node,
}
}

/// Whether this component is definitively incompatible (a `Fail` verdict);
/// `Unknown`/no-verdict is not treated as incompatible.
/// The advisory runtime-API verdict — never gates compatibility.
pub fn api_advisory(&self) -> &CompatVerdict {
match self {
DetectionResult::WebApp { api, .. } => api,
DetectionResult::Service { api, .. } => api,
}
}

/// Whether this component is definitively incompatible (a gating `Fail`);
/// `Unknown` and the advisory API verdict never count.
pub fn is_incompatible(&self) -> bool {
matches!(self.verdict(), Some(CompatVerdict::Fail { .. }))
matches!(self.verdict(), CompatVerdict::Fail { .. })
}
}

Expand Down Expand Up @@ -134,11 +150,17 @@ impl VerifyForFirmware for Package {

fn web_detection(app: &Component<AppInfo>, engine: Option<&WebEngine>) -> Option<DetectionResult> {
let detection = app.info.web.clone()?;
let es = web_verdict(detection.es_level, engine);
let es = web_verdict(detection.es_level, engine, "app uses");
// Advisory: highest ES level implied by the runtime APIs used.
let api_level = detection.es_apis.iter().map(|a| a.level).max();
let api = web_verdict(api_level, engine, "app calls APIs from").demote_reason(
"may need polyfills",
);
return Some(DetectionResult::WebApp {
detection,
engine: engine.cloned(),
es,
api,
});
}

Expand All @@ -147,34 +169,81 @@ fn service_detection(
node: Option<&Version>,
) -> Option<DetectionResult> {
let detection = svc.info.runtime.clone()?;
let node_verdict = service_verdict(detection.es_level, node, "service uses");
let api_level = detection.es_apis.iter().map(|a| a.level).max();
let api = service_verdict(api_level, node, "service calls APIs from")
.demote_reason("may need polyfills");
return Some(DetectionResult::Service {
detection,
available_node: node.cloned(),
node: node_verdict,
api,
});
}

/// Does the firmware's web engine support the app's required ES level?
fn web_verdict(es_level: Option<EsLevel>, engine: Option<&WebEngine>) -> CompatVerdict {
/// Whether the firmware's web engine supports the given ES level.
fn web_verdict(es_level: Option<EsLevel>, engine: Option<&WebEngine>, verb: &str) -> CompatVerdict {
let Some(es_level) = es_level else {
return CompatVerdict::Unknown;
};
let fw_max = match engine {
Some(WebEngine::Chromium(v)) => EsLevel::from_chromium_major(v.major as u32),
// The LG WebKit port (537.x) predates reliable ES2015 support.
Some(WebEngine::WebKit(_)) => EsLevel::Es5,
None => return CompatVerdict::Unknown,
let Some(engine) = engine else {
return CompatVerdict::Unknown;
};
let fw_max = engine_max_es(engine);
if es_level <= fw_max {
CompatVerdict::Ok
} else {
CompatVerdict::Fail {
reason: format!(
"app uses {}, but {} supports up to {}",
"{verb} {}, but {} supports up to {}",
es_level.label(),
engine.map(|e| e.label()).unwrap_or_default(),
engine.label(),
fw_max.label()
),
}
}
}

/// Whether the firmware's Node.js supports the given ES level.
fn service_verdict(es_level: Option<EsLevel>, node: Option<&Version>, verb: &str) -> CompatVerdict {
let Some(es_level) = es_level else {
return CompatVerdict::Unknown;
};
let Some(node) = node else {
return CompatVerdict::Unknown;
};
let (maj, min) = es_level.min_node_version();
if node.major > maj || (node.major == maj && node.minor >= min) {
CompatVerdict::Ok
} else {
CompatVerdict::Fail {
reason: format!(
"{verb} {}, which needs Node.js {maj}.{min}, but firmware ships {node}",
es_level.label(),
),
}
}
}

/// The highest ES level a firmware's web engine supports.
pub fn engine_max_es(engine: &WebEngine) -> EsLevel {
match engine {
WebEngine::Chromium(v) => EsLevel::from_chromium_major(v.major as u32),
// The LG WebKit port (537.x) predates reliable ES2015 support.
WebEngine::WebKit(_) => EsLevel::Es5,
}
}

impl CompatVerdict {
/// Rewrite a `Fail` reason's lead-in so the advisory reads as a polyfill
/// note rather than a hard failure. No-op for `Ok`/`Unknown`.
fn demote_reason(self, note: &str) -> CompatVerdict {
match self {
CompatVerdict::Fail { reason } => CompatVerdict::Fail {
reason: format!("{reason} ({note})"),
},
other => other,
}
}
}

25 changes: 25 additions & 0 deletions common/webdetect/src/eslevel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ impl EsLevel {
EsLevel::Es5
}

/// Minimum Node.js major/minor that supports the syntax this level implies
/// (V8 landing points). Used to check a service's own code against the
/// firmware's bundled Node.js — a trustworthy, code-derived requirement
/// (unlike `engines.node`, which isn't read).
pub fn min_node_version(self) -> (u64, u64) {
match self {
EsLevel::Es5 => (0, 10),
EsLevel::Es2015 => (6, 0), // let/const/arrow/class/template/spread
EsLevel::Es2016 => (7, 0), // ** exponentiation
EsLevel::Es2017 => (7, 6), // async/await
EsLevel::Es2018 => (10, 0), // object spread, async iteration
EsLevel::Es2019 => (12, 0),
EsLevel::Es2020 => (14, 0), // optional chaining, nullish coalescing
EsLevel::Es2021Plus => (15, 0),
}
}

pub fn label(self) -> &'static str {
match self {
EsLevel::Es5 => "ES5",
Expand Down Expand Up @@ -136,6 +153,14 @@ mod tests {
assert_eq!(EsLevel::from_chromium_major(0), EsLevel::Es5);
}

#[test]
fn min_node_versions_are_ordered() {
assert_eq!(EsLevel::Es5.min_node_version(), (0, 10));
assert_eq!(EsLevel::Es2017.min_node_version(), (7, 6));
assert_eq!(EsLevel::Es2020.min_node_version(), (14, 0));
assert!(EsLevel::Es2020.min_node_version() > EsLevel::Es2015.min_node_version());
}

#[test]
fn feature_levels_order() {
assert!(EsFeature::OptionalChaining.level() > EsFeature::AsyncAwait.level());
Expand Down
Loading
Loading