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
40 changes: 40 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"common/bin",
"common/ipk",
"common/verify",
"common/webdetect",
"packages/elf-verify",
"packages/ipk-verify",
"packages/fw-extract",
Expand Down
10 changes: 9 additions & 1 deletion common/fw/src/firmware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};

use bin_lib::LibraryInfo;

use crate::{Firmware, FirmwareInfo, ReleaseCodename};
use crate::{Firmware, FirmwareInfo, PackageEntry, ReleaseCodename};

impl FirmwareInfo {
pub fn codename(&self) -> Option<ReleaseCodename> {
Expand Down Expand Up @@ -74,10 +74,18 @@ impl Firmware {
});
})?;

// packages.json carries the installed OS package versions (Node.js, the
// web engine, ...). Older data dumps may lack it, so tolerate absence.
let packages: HashMap<String, PackageEntry> = File::open(path.join("packages.json"))
.ok()
.and_then(|file| serde_json::from_reader(BufReader::new(file)).ok())
.unwrap_or_default();

Ok(Firmware {
path: path.to_path_buf(),
info,
index,
packages,
})
}

Expand Down
18 changes: 18 additions & 0 deletions common/fw/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ use std::collections::HashMap;
use std::path::PathBuf;

pub mod firmware;
pub mod runtime;
pub(crate) mod version;
use version::version_deserialize;
use version::version_serialize;

pub use runtime::WebEngine;

#[derive(Debug, Serialize, Deserialize)]
pub struct FirmwareInfo {
pub version: String,
Expand All @@ -24,6 +27,21 @@ pub struct Firmware {
pub info: FirmwareInfo,
path: PathBuf,
index: HashMap<String, String>,
packages: HashMap<String, PackageEntry>,
}

/// One entry in a firmware's `packages.json`, e.g.
/// `"lib32-nodejs": { "version": { "upstream": "16.20.2", ... } }`.
#[derive(Debug, Deserialize)]
pub struct PackageEntry {
pub version: PackageVersion,
}

#[derive(Debug, Deserialize)]
pub struct PackageVersion {
pub upstream: String,
#[serde(default)]
pub debian_revision: Option<String>,
}

pub enum ReleaseCodename {
Expand Down
168 changes: 168 additions & 0 deletions common/fw/src/runtime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Per-firmware runtime versions (Node.js, web engine) read from `packages.json`.
//!
//! webOS ships the JS-service Node.js runtime and the web-app engine as ordinary
//! OS packages, so their versions live in each firmware's `packages.json` rather
//! than in a table we would have to maintain by hand. The package *names* vary by
//! generation, so resolution tries a family of candidates.

use semver::Version;

use crate::Firmware;

/// The web-app rendering engine a firmware ships. webOS has used two families:
/// a modern Chromium-based runtime (WAM) and, on the earliest TVs, an LG WebKit
/// port (`webkit-starfish`, versioned like `537.41`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebEngine {
Chromium(Version),
WebKit(Version),
}

impl WebEngine {
/// Human-readable label, e.g. "Chromium 120" or "WebKit 537.41".
pub fn label(&self) -> String {
match self {
WebEngine::Chromium(v) => format!("Chromium {}", v.major),
WebEngine::WebKit(v) => format!("WebKit {}.{}", v.major, v.minor),
}
}
}

impl Firmware {
/// The Node.js version this firmware ships, if a node package is present.
pub fn node_version(&self) -> Option<Version> {
["lib32-nodejs", "nodejs"]
.iter()
.find_map(|name| self.pkg_version(name))
}

/// The web-app engine this firmware ships. Resolution order:
/// 1. Chromium family: `lib32-webruntime` → `webruntime` → highest
/// `chromium<NN>` → `chromium-webos` → `chromium`;
/// 2. WebKit family: `webkit-starfish` → `qt5-qtwebkit` → `libQt5WebKit`.
///
/// `com.webos.app.browser` is deliberately ignored — it is the built-in
/// browser *app*, not the web-app runtime.
pub fn web_engine(&self) -> Option<WebEngine> {
for name in ["lib32-webruntime", "webruntime"] {
if let Some(v) = self.pkg_version(name) {
return Some(WebEngine::Chromium(v));
}
}
if let Some(v) = self.highest_numbered_chromium() {
return Some(WebEngine::Chromium(v));
}
for name in ["chromium-webos", "chromium"] {
if let Some(v) = self.pkg_version(name) {
return Some(WebEngine::Chromium(v));
}
}
for name in ["webkit-starfish", "qt5-qtwebkit", "libQt5WebKit"] {
if let Some(v) = self.pkg_version(name) {
return Some(WebEngine::WebKit(v));
}
}
None
}

/// Parse a package's `upstream` version string into a [`Version`].
fn pkg_version(&self, name: &str) -> Option<Version> {
self.packages
.get(name)
.and_then(|entry| parse_leading_semver(&entry.version.upstream))
}

/// Among version-suffixed `chromium<NN>` packages (`chromium38`,
/// `chromium53`, ...), the one with the highest `NN`.
fn highest_numbered_chromium(&self) -> Option<Version> {
let mut best: Option<(u32, &str)> = None;
for key in self.packages.keys() {
if let Some(rest) = key.strip_prefix("chromium") {
if rest.is_empty() || !rest.bytes().all(|b| b.is_ascii_digit()) {
continue;
}
if let Ok(n) = rest.parse::<u32>() {
if best.map_or(true, |(b, _)| n > b) {
best = Some((n, key));
}
}
}
}
best.and_then(|(_, key)| self.pkg_version(key))
}
}

/// Take the leading dotted-numeric run of an `upstream` string and parse it as a
/// three-component [`Version`]. Debian `upstream` strings are frequently longer
/// than semver allows (`120.0.6099.270-137.paparoa.1`) or have a `-suffix`
/// (`53.0.2785.34-92...`), so keep only the first three numeric parts.
fn parse_leading_semver(upstream: &str) -> Option<Version> {
let lead: String = upstream
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
let mut parts = lead.split('.').filter(|s| !s.is_empty());
let major = parts.next()?.parse().ok()?;
let minor = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let patch = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
Some(Version::new(major, minor, patch))
}

#[cfg(test)]
mod tests {
use super::*;
use crate::Firmware;
use std::collections::HashMap;
use std::path::PathBuf;

#[test]
fn parses_various_upstream_strings() {
assert_eq!(parse_leading_semver("16.20.2"), Some(Version::new(16, 20, 2)));
assert_eq!(parse_leading_semver("0.10.15"), Some(Version::new(0, 10, 15)));
assert_eq!(
parse_leading_semver("120.0.6099.270-137.paparoa.1"),
Some(Version::new(120, 0, 6099))
);
assert_eq!(
parse_leading_semver("53.0.2785.34-92.323.glacier.45"),
Some(Version::new(53, 0, 2785))
);
assert_eq!(
parse_leading_semver("537.41-420.afro.2"),
Some(Version::new(537, 41, 0))
);
assert_eq!(parse_leading_semver("garbage"), None);
}

/// Load every committed firmware data dir and assert the runtime accessors
/// resolve the packages we validated by hand.
#[test]
fn resolves_runtimes_from_real_data() {
let data = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../data");
if !data.exists() {
return; // data may be absent in some checkouts
}
let firmwares = Firmware::list(&data).expect("list firmwares");
let by_release: HashMap<String, &Firmware> = firmwares
.iter()
.map(|f| (f.info.release.to_string(), f))
.collect();

let engine = |rel: &str| by_release.get(rel).and_then(|f| f.web_engine());
let node = |rel: &str| by_release.get(rel).and_then(|f| f.node_version());

// Chromium generations, including the version-suffixed names.
assert_eq!(engine("10.2.0"), Some(WebEngine::Chromium(Version::new(120, 0, 6099))));
assert_eq!(engine("4.4.2"), Some(WebEngine::Chromium(Version::new(53, 0, 2785))));
assert_eq!(engine("3.4.0"), Some(WebEngine::Chromium(Version::new(38, 0, 2125))));
assert_eq!(engine("1.2.0"), Some(WebEngine::Chromium(Version::new(26, 0, 1410))));

// Pre-Chromium WebKit images.
assert_eq!(engine("2.2.3"), Some(WebEngine::WebKit(Version::new(537, 41, 0))));
assert!(matches!(engine("1.4.0"), Some(WebEngine::WebKit(_))));

// Node.js.
assert_eq!(node("10.2.0"), Some(Version::new(16, 20, 2)));
assert_eq!(node("6.4.0"), Some(Version::new(8, 12, 0)));
}
}
5 changes: 4 additions & 1 deletion common/ipk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ path-slash = { workspace = true }
common-path = "1.0.0"

[dependencies.bin-lib]
path = "../bin"
path = "../bin"

[dependencies.webdetect-lib]
path = "../webdetect"
17 changes: 14 additions & 3 deletions common/ipk/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use path_slash::CowExt;

use bin_lib::{BinaryInfo, LibraryInfo, LibraryPriority};

use crate::path::ensure_within;
use crate::{AppInfo, Component, ServiceInfo, Symlinks};

impl AppInfo {
Expand Down Expand Up @@ -41,14 +42,20 @@ impl Component<AppInfo> {
)
})?;
if !info.is_native() {
// Web/hosted app: detect the frontend framework and JS syntax level
// from the shipped HTML/JS while the extracted files still exist.
// `main` is untrusted; keep it inside the app directory.
let index_html = ensure_within(dir, &dir.join(Cow::from_slash(&info.main)))?;
let mut info = info;
info.web = Some(webdetect_lib::detect_web_app(dir, &index_html));
return Ok(Self {
id: info.id.clone(),
info,
exe: Default::default(),
libs: Default::default(),
});
}
let exe_path = dir.join(Cow::from_slash(&info.main));
let exe_path = ensure_within(dir, &dir.join(Cow::from_slash(&info.main)))?;
let bin_info = BinaryInfo::parse(
File::open(&exe_path).map_err(|e| {
Error::new(
Expand Down Expand Up @@ -84,6 +91,10 @@ impl Component<ServiceInfo> {
let info: ServiceInfo = serde_json::from_reader(File::open(dir.join("services.json"))?)
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("Bad appinfo.json: {e:?}")))?;
if !info.is_native() {
// JS/Node service: detect the declared Node.js runtime from the
// bundled package.json while the extracted files still exist.
let mut info = info;
info.runtime = Some(webdetect_lib::detect_service_runtime(dir));
return Ok(Self {
id: info.id.clone(),
info: info.clone(),
Expand All @@ -92,9 +103,9 @@ impl Component<ServiceInfo> {
});
}
let executable = info.executable.as_ref().unwrap();
let exe_path = dir.join(Cow::from_slash(executable));
let exe_path = ensure_within(dir, &dir.join(Cow::from_slash(executable)))?;
let bin_info = BinaryInfo::parse(
File::open(dir.join(&exe_path))?,
File::open(&exe_path)?,
exe_path.file_name().unwrap().to_string_lossy(),
true,
)
Expand Down
Loading
Loading