fixes from testing, client side device filtering for better fingerprinting resistance

This commit is contained in:
Aiden McClelland
2026-01-14 18:04:14 -07:00
parent 359c7a89bf
commit ffa801ff6d
29 changed files with 933 additions and 682 deletions

View File

@@ -4,7 +4,7 @@ parse_essential_db_info() {
DB_DUMP="/tmp/startos_db.json" DB_DUMP="/tmp/startos_db.json"
if command -v start-cli >/dev/null 2>&1; then if command -v start-cli >/dev/null 2>&1; then
start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1 timeout 30 start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
else else
return 1 return 1
fi fi

661
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ use std::sync::Arc;
use cookie::{Cookie, Expiration, SameSite}; use cookie::{Cookie, Expiration, SameSite};
use cookie_store::CookieStore; use cookie_store::CookieStore;
use http::HeaderMap; use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use josekit::jwk::Jwk; use josekit::jwk::Jwk;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@@ -238,10 +239,16 @@ impl CliContext {
where where
Self: CallRemote<RemoteContext>, Self: CallRemote<RemoteContext>,
{ {
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {}) <Self as CallRemote<RemoteContext, Empty>>::call_remote(
.await &self,
.map_err(Error::from) method,
.with_ctx(|e| (e.kind, method)) OrdMap::new(),
params,
Empty {},
)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
} }
pub async fn call_remote_with<RemoteContext, T>( pub async fn call_remote_with<RemoteContext, T>(
&self, &self,
@@ -252,10 +259,16 @@ impl CliContext {
where where
Self: CallRemote<RemoteContext, T>, Self: CallRemote<RemoteContext, T>,
{ {
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra) <Self as CallRemote<RemoteContext, T>>::call_remote(
.await &self,
.map_err(Error::from) method,
.with_ctx(|e| (e.kind, method)) OrdMap::new(),
params,
extra,
)
.await
.map_err(Error::from)
.with_ctx(|e| (e.kind, method))
} }
} }
impl AsRef<Jwk> for CliContext { impl AsRef<Jwk> for CliContext {
@@ -292,7 +305,13 @@ impl AsRef<Client> for CliContext {
} }
impl CallRemote<RpcContext> for CliContext { impl CallRemote<RpcContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await { if let Ok(local) = read_file_to_string(RpcContext::LOCAL_AUTH_COOKIE_PATH).await {
self.cookie_store self.cookie_store
.lock() .lock()
@@ -319,7 +338,13 @@ impl CallRemote<RpcContext> for CliContext {
} }
} }
impl CallRemote<DiagnosticContext> for CliContext { impl CallRemote<DiagnosticContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),
@@ -332,7 +357,13 @@ impl CallRemote<DiagnosticContext> for CliContext {
} }
} }
impl CallRemote<InitContext> for CliContext { impl CallRemote<InitContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),
@@ -345,7 +376,13 @@ impl CallRemote<InitContext> for CliContext {
} }
} }
impl CallRemote<SetupContext> for CliContext { impl CallRemote<SetupContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),
@@ -358,7 +395,13 @@ impl CallRemote<SetupContext> for CliContext {
} }
} }
impl CallRemote<InstallContext> for CliContext { impl CallRemote<InstallContext> for CliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote( crate::middleware::auth::signature::call_remote(
self, self,
self.rpc_url.clone(), self.rpc_url.clone(),

View File

@@ -29,7 +29,6 @@ use crate::db::model::package::TaskSeverity;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::loop_dev::LoopDev;
use crate::disk::mount::filesystem::{FileSystem, ReadOnly}; use crate::disk::mount::filesystem::{FileSystem, ReadOnly};
use crate::disk::mount::guard::MountGuard; use crate::disk::mount::guard::MountGuard;
use crate::init::{InitResult, check_time_is_synchronized}; use crate::init::{InitResult, check_time_is_synchronized};
@@ -586,8 +585,14 @@ impl RpcContext {
where where
Self: CallRemote<RemoteContext>, Self: CallRemote<RemoteContext>,
{ {
<Self as CallRemote<RemoteContext, Empty>>::call_remote(&self, method, params, Empty {}) <Self as CallRemote<RemoteContext, Empty>>::call_remote(
.await &self,
method,
OrdMap::new(),
params,
Empty {},
)
.await
} }
pub async fn call_remote_with<RemoteContext, T>( pub async fn call_remote_with<RemoteContext, T>(
&self, &self,
@@ -598,7 +603,14 @@ impl RpcContext {
where where
Self: CallRemote<RemoteContext, T>, Self: CallRemote<RemoteContext, T>,
{ {
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, params, extra).await <Self as CallRemote<RemoteContext, T>>::call_remote(
&self,
method,
OrdMap::new(),
params,
extra,
)
.await
} }
} }
impl AsRef<Client> for RpcContext { impl AsRef<Client> for RpcContext {

View File

@@ -416,6 +416,51 @@ impl<T: Map> Model<T> {
} }
} }
impl<T: Map> Model<T>
where
T::Key: FromStr,
Error: From<<T::Key as FromStr>::Err>,
{
/// Retains only the elements specified by the predicate.
/// The predicate can mutate the values and returns whether to keep each entry.
pub fn retain<F>(&mut self, mut f: F) -> Result<(), Error>
where
F: FnMut(&T::Key, &mut Model<T::Value>) -> Result<bool, Error>,
{
let mut to_remove = Vec::new();
match &mut self.value {
Value::Object(o) => {
for (k, v) in o.iter_mut() {
let key = T::Key::from_str(&**k)?;
if !f(&key, patch_db::ModelExt::value_as_mut(v))? {
to_remove.push(k.clone());
}
}
}
v => {
use serde::de::Error;
return Err(patch_db::value::Error {
source: patch_db::value::ErrorSource::custom(format!(
"expected object found {v}"
)),
kind: patch_db::value::ErrorKind::Deserialization,
}
.into());
}
}
// Remove entries that didn't pass the filter
if let Value::Object(o) = &mut self.value {
for k in to_remove {
o.remove(&k);
}
}
Ok(())
}
}
#[repr(transparent)] #[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct JsonKey<T>(pub T); pub struct JsonKey<T>(pub T);

View File

@@ -7,6 +7,7 @@ use chrono::Utc;
use clap::Parser; use clap::Parser;
use cookie::{Cookie, Expiration, SameSite}; use cookie::{Cookie, Expiration, SameSite};
use http::HeaderMap; use http::HeaderMap;
use imbl::OrdMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use patch_db::PatchDb; use patch_db::PatchDb;
use patch_db::json_ptr::ROOT; use patch_db::json_ptr::ROOT;
@@ -171,6 +172,7 @@ impl CallRemote<RegistryContext> for CliContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value, params: Value,
_: Empty, _: Empty,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
@@ -240,14 +242,21 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
metadata: OrdMap<&'static str, Value>,
params: Value, params: Value,
RegistryUrlParams { mut registry }: RegistryUrlParams, RegistryUrlParams { mut registry }: RegistryUrlParams,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert( let mut device_info = None;
DEVICE_INFO_HEADER, if metadata
DeviceInfo::load(self).await?.to_header_value(), .get("get_device_info")
); .and_then(|m| m.as_bool())
.unwrap_or(false)
{
let di = DeviceInfo::load(self).await?;
headers.insert(DEVICE_INFO_HEADER, di.to_header_value());
device_info = Some(di);
}
registry registry
.path_segments_mut() .path_segments_mut()
@@ -258,15 +267,21 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
method = method.strip_prefix("registry.").unwrap_or(method); method = method.strip_prefix("registry.").unwrap_or(method);
let sig_context = registry.host_str().map(InternedString::from); let sig_context = registry.host_str().map(InternedString::from);
crate::middleware::auth::signature::call_remote( let mut res = crate::middleware::auth::signature::call_remote(
self, self,
registry, registry,
headers, headers,
sig_context.as_deref(), sig_context.as_deref(),
method, method,
params, params.clone(),
) )
.await .await?;
if let Some(device_info) = device_info {
device_info.filter_for_hardware(method, params, &mut res)?;
}
Ok(res)
} }
} }

View File

@@ -1,5 +1,4 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::convert::identity;
use std::ops::Deref; use std::ops::Deref;
use axum::extract::Request; use axum::extract::Request;
@@ -7,6 +6,8 @@ use axum::response::Response;
use exver::{Version, VersionRange}; use exver::{Version, VersionRange};
use http::HeaderValue; use http::HeaderValue;
use imbl_value::InternedString; use imbl_value::InternedString;
use patch_db::ModelExt;
use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
@@ -15,8 +16,13 @@ use url::Url;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::prelude::*; use crate::prelude::*;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfoMap;
use crate::registry::package::get::{
GetPackageParams, GetPackageResponse, GetPackageResponseFull, PackageDetailLevel,
};
use crate::registry::package::index::PackageVersionInfo;
use crate::util::VersionString; use crate::util::VersionString;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor}; use crate::util::lshw::LshwDevice;
use crate::version::VersionT; use crate::version::VersionT;
pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info"; pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
@@ -25,13 +31,13 @@ pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DeviceInfo { pub struct DeviceInfo {
pub os: OsInfo, pub os: OsInfo,
pub hardware: HardwareInfo, pub hardware: Option<HardwareInfo>,
} }
impl DeviceInfo { impl DeviceInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> { pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
Ok(Self { Ok(Self {
os: OsInfo::from(ctx), os: OsInfo::from(ctx),
hardware: HardwareInfo::load(ctx).await?, hardware: Some(HardwareInfo::load(ctx).await?),
}) })
} }
} }
@@ -41,21 +47,13 @@ impl DeviceInfo {
url.query_pairs_mut() url.query_pairs_mut()
.append_pair("os.version", &self.os.version.to_string()) .append_pair("os.version", &self.os.version.to_string())
.append_pair("os.compat", &self.os.compat.to_string()) .append_pair("os.compat", &self.os.compat.to_string())
.append_pair("os.platform", &*self.os.platform) .append_pair("os.platform", &*self.os.platform);
.append_pair("hardware.arch", &*self.hardware.arch)
.append_pair("hardware.ram", &self.hardware.ram.to_string());
for device in &self.hardware.devices {
url.query_pairs_mut().append_pair(
&format!("hardware.device.{}", device.class()),
device.product(),
);
}
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
} }
pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> { pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> {
let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
let has_hw_info = query.keys().any(|k| k.starts_with("hardware."));
Ok(Self { Ok(Self {
os: OsInfo { os: OsInfo {
version: query version: query
@@ -69,35 +67,120 @@ impl DeviceInfo {
.deref() .deref()
.into(), .into(),
}, },
hardware: HardwareInfo { hardware: has_hw_info
arch: query .then(|| {
.get("hardware.arch") Ok::<_, Error>(HardwareInfo {
.or_not_found("hardware.arch")? arch: query
.parse()?, .get("hardware.arch")
ram: query .or_not_found("hardware.arch")?
.get("hardware.ram") .parse()?,
.or_not_found("hardware.ram")? ram: query
.parse()?, .get("hardware.ram")
devices: identity(query) .or_not_found("hardware.ram")?
.split_off("hardware.device.") .parse()?,
.into_iter() devices: None,
.filter_map(|(k, v)| match k.strip_prefix("hardware.device.") {
Some("processor") => Some(LshwDevice::Processor(LshwProcessor {
product: v.into_owned(),
})),
Some("display") => Some(LshwDevice::Display(LshwDisplay {
product: v.into_owned(),
})),
Some(class) => {
tracing::warn!("unknown device class: {class}");
None
}
_ => None,
}) })
.collect(), })
}, .transpose()?,
}) })
} }
pub fn filter_for_hardware(
&self,
method: &str,
params: Value,
res: &mut Value,
) -> Result<(), Error> {
match method {
"package.get" => {
let params: Model<GetPackageParams> = ModelExt::from_value(params);
let other = params.as_other_versions().de()?;
if params.as_id().transpose_ref().is_some() {
if other.unwrap_or_default() == PackageDetailLevel::Full {
self.filter_package_get_full(ModelExt::value_as_mut(res))?;
} else {
self.filter_package_get(ModelExt::value_as_mut(res))?;
}
} else {
for (_, v) in res.as_object_mut().into_iter().flat_map(|o| o.iter_mut()) {
if other.unwrap_or_default() == PackageDetailLevel::Full {
self.filter_package_get_full(ModelExt::value_as_mut(v))?;
} else {
self.filter_package_get(ModelExt::value_as_mut(v))?;
}
}
}
Ok(())
}
"os.version.get" => self.filter_os_version(ModelExt::value_as_mut(res)),
_ => Ok(()),
}
}
fn filter_package_versions(
&self,
versions: &mut Model<BTreeMap<VersionString, PackageVersionInfo>>,
) -> Result<(), Error> {
let alpha_17: Version = "0.4.0-alpha.17".parse()?;
// Filter package versions using for_device
versions.retain(|_, info| info.for_device(self))?;
// Alpha.17 compatibility: add legacy fields
if self.os.version <= alpha_17 {
for (_, info) in versions.as_entries_mut()? {
let v = info.as_value_mut();
if let Some(mut tup) = v["s9pks"].get(0).cloned() {
v["s9pk"] = tup[1].take();
v["hardwareRequirements"] = tup[0].take();
v["s9pk"]["url"] = v["s9pk"]["urls"][0].clone();
}
}
}
Ok(())
}
fn filter_package_get(&self, res: &mut Model<GetPackageResponse>) -> Result<(), Error> {
self.filter_package_versions(res.as_best_mut())
}
fn filter_package_get_full(
&self,
res: &mut Model<GetPackageResponseFull>,
) -> Result<(), Error> {
self.filter_package_versions(res.as_best_mut())?;
self.filter_package_versions(res.as_other_versions_mut())
}
fn filter_os_version(&self, res: &mut Model<OsVersionInfoMap>) -> Result<(), Error> {
let alpha_17: Version = "0.4.0-alpha.17".parse()?;
// Filter OS versions based on source_version compatibility
res.retain(|_, info| {
let source_version = info.as_source_version().de()?;
Ok(self.os.version.satisfies(&source_version))
})?;
// Alpha.17 compatibility: add url field from urls array
if self.os.version <= alpha_17 {
for (_, info) in res.as_entries_mut()? {
let v = info.as_value_mut();
for asset_ty in ["iso", "squashfs", "img"] {
for (_, asset) in v[asset_ty]
.as_object_mut()
.into_iter()
.flat_map(|o| o.iter_mut())
{
asset["url"] = asset["urls"][0].clone();
}
}
}
}
Ok(())
}
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
@@ -127,7 +210,7 @@ pub struct HardwareInfo {
pub arch: InternedString, pub arch: InternedString,
#[ts(type = "number")] #[ts(type = "number")]
pub ram: u64, pub ram: u64,
pub devices: Vec<LshwDevice>, pub devices: Option<Vec<LshwDevice>>,
} }
impl HardwareInfo { impl HardwareInfo {
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> { pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
@@ -135,7 +218,7 @@ impl HardwareInfo {
Ok(Self { Ok(Self {
arch: s.as_arch().de()?, arch: s.as_arch().de()?,
ram: s.as_ram().de()?, ram: s.as_ram().de()?,
devices: s.as_devices().de()?, devices: Some(s.as_devices().de()?),
}) })
} }
} }
@@ -148,11 +231,17 @@ pub struct Metadata {
#[derive(Clone)] #[derive(Clone)]
pub struct DeviceInfoMiddleware { pub struct DeviceInfoMiddleware {
device_info: Option<HeaderValue>, device_info_header: Option<HeaderValue>,
device_info: Option<DeviceInfo>,
req: Option<RpcRequest>,
} }
impl DeviceInfoMiddleware { impl DeviceInfoMiddleware {
pub fn new() -> Self { pub fn new() -> Self {
Self { device_info: None } Self {
device_info_header: None,
device_info: None,
req: None,
}
} }
} }
@@ -163,7 +252,7 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
_: &RegistryContext, _: &RegistryContext,
request: &mut Request, request: &mut Request,
) -> Result<(), Response> { ) -> Result<(), Response> {
self.device_info = request.headers_mut().remove(DEVICE_INFO_HEADER); self.device_info_header = request.headers_mut().remove(DEVICE_INFO_HEADER);
Ok(()) Ok(())
} }
async fn process_rpc_request( async fn process_rpc_request(
@@ -174,9 +263,11 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
) -> Result<(), RpcResponse> { ) -> Result<(), RpcResponse> {
async move { async move {
if metadata.get_device_info { if metadata.get_device_info {
if let Some(device_info) = &self.device_info { if let Some(device_info) = &self.device_info_header {
request.params["__DeviceInfo_device_info"] = let device_info = DeviceInfo::from_header_value(device_info)?;
to_value(&DeviceInfo::from_header_value(device_info)?)?; request.params["__DeviceInfo_device_info"] = to_value(&device_info)?;
self.device_info = Some(device_info);
self.req = Some(request.clone());
} }
} }
@@ -185,4 +276,19 @@ impl Middleware<RegistryContext> for DeviceInfoMiddleware {
.await .await
.map_err(|e| RpcResponse::from_result(Err(e))) .map_err(|e| RpcResponse::from_result(Err(e)))
} }
async fn process_rpc_response(
&mut self,
_: &RegistryContext,
response: &mut RpcResponse,
) -> () {
if let (Some(req), Some(device_info), Ok(res)) =
(&self.req, &self.device_info, &mut response.result)
{
if let Err(e) =
device_info.filter_for_hardware(req.method.as_str(), req.params.clone(), res)
{
response.result = Err(e).map_err(From::from);
}
}
}
} }

View File

@@ -54,7 +54,7 @@ pub async fn add_package(
let peek = ctx.db.peek().await; let peek = ctx.db.peek().await;
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?; let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?;
let ([url], rest) = urls.split_at(1) else { let Some(([url], rest)) = urls.split_at_checked(1) else {
return Err(Error::new( return Err(Error::new(
eyre!("must specify at least 1 url"), eyre!("must specify at least 1 url"),
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,

View File

@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use exver::{ExtendedVersion, Version, VersionRange}; use exver::{ExtendedVersion, VersionRange};
use imbl_value::{InternedString, json}; use imbl_value::{InternedString, json};
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -12,14 +12,11 @@ use crate::PackageId;
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgressTracker, ProgressUnits}; use crate::progress::{FullProgressTracker, ProgressUnits};
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::device_info::DeviceInfo; use crate::registry::device_info::DeviceInfo;
use crate::registry::package::index::{PackageIndex, PackageVersionInfo}; use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
use crate::s9pk::manifest::HardwareRequirements;
use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::util::VersionString; use crate::util::VersionString;
use crate::util::io::{TrackingIO, to_tmp_path}; use crate::util::io::{TrackingIO, to_tmp_path};
use crate::util::serde::{WithIoFormat, display_serializable}; use crate::util::serde::{WithIoFormat, display_serializable};
@@ -28,7 +25,7 @@ use crate::util::tui::{choose, choose_custom_display};
#[derive( #[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum,
)] )]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "kebab-case")]
#[ts(export)] #[ts(export)]
pub enum PackageDetailLevel { pub enum PackageDetailLevel {
None, None,
@@ -48,10 +45,11 @@ pub struct PackageInfoShort {
pub release_notes: String, pub release_notes: String,
} }
#[derive(Debug, Deserialize, Serialize, TS, Parser)] #[derive(Debug, Deserialize, Serialize, TS, Parser, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageParams { pub struct GetPackageParams {
pub id: Option<PackageId>, pub id: Option<PackageId>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
@@ -63,14 +61,14 @@ pub struct GetPackageParams {
#[arg(skip)] #[arg(skip)]
#[serde(rename = "__DeviceInfo_device_info")] #[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>, pub device_info: Option<DeviceInfo>,
#[serde(default)]
#[arg(default_value = "none")] #[arg(default_value = "none")]
pub other_versions: PackageDetailLevel, pub other_versions: Option<PackageDetailLevel>,
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageResponse { pub struct GetPackageResponse {
#[ts(type = "string[]")] #[ts(type = "string[]")]
pub categories: BTreeSet<InternedString>, pub categories: BTreeSet<InternedString>,
@@ -111,9 +109,10 @@ impl GetPackageResponse {
} }
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageResponseFull { pub struct GetPackageResponseFull {
#[ts(type = "string[]")] #[ts(type = "string[]")]
pub categories: BTreeSet<InternedString>, pub categories: BTreeSet<InternedString>,
@@ -137,23 +136,15 @@ impl GetPackageResponseFull {
pub type GetPackagesResponse = BTreeMap<PackageId, GetPackageResponse>; pub type GetPackagesResponse = BTreeMap<PackageId, GetPackageResponse>;
pub type GetPackagesResponseFull = BTreeMap<PackageId, GetPackageResponseFull>; pub type GetPackagesResponseFull = BTreeMap<PackageId, GetPackageResponseFull>;
fn get_matching_models<'a>( fn get_matching_models(
db: &'a Model<PackageIndex>, db: &Model<PackageIndex>,
GetPackageParams { GetPackageParams {
id, id,
source_version, source_version,
device_info, device_info,
.. ..
}: &GetPackageParams, }: &GetPackageParams,
) -> Result< ) -> Result<Vec<(PackageId, ExtendedVersion, Model<PackageVersionInfo>)>, Error> {
Vec<(
PackageId,
ExtendedVersion,
&'a Model<PackageVersionInfo>,
Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>,
)>,
Error,
> {
if let Some(id) = id { if let Some(id) = id {
if let Some(pkg) = db.as_packages().as_idx(id) { if let Some(pkg) = db.as_packages().as_idx(id) {
vec![(id.clone(), pkg)] vec![(id.clone(), pkg)]
@@ -180,12 +171,16 @@ fn get_matching_models<'a>(
), ),
) )
})? { })? {
let mut info = info.clone();
if let Some(device_info) = &device_info { if let Some(device_info) = &device_info {
info.for_device(device_info)? if info.for_device(device_info)? {
Some((k.clone(), ExtendedVersion::from(v), info))
} else {
None
}
} else { } else {
Some(info.as_s9pks().de()?) Some((k.clone(), ExtendedVersion::from(v), info))
} }
.map(|assets| (k.clone(), ExtendedVersion::from(v), info, assets))
} else { } else {
None None
}, },
@@ -199,31 +194,12 @@ fn get_matching_models<'a>(
} }
pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result<Value, Error> { pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result<Value, Error> {
use patch_db::ModelExt;
let peek = ctx.db.peek().await; let peek = ctx.db.peek().await;
let mut best: BTreeMap< let mut best: BTreeMap<PackageId, BTreeMap<VersionString, Model<PackageVersionInfo>>> =
PackageId, Default::default();
BTreeMap< let mut other: BTreeMap<PackageId, BTreeMap<VersionString, Model<PackageVersionInfo>>> =
VersionString, Default::default();
( for (id, version, info) in get_matching_models(&peek.as_index().as_package(), &params)? {
&Model<PackageVersionInfo>,
Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>,
),
>,
> = Default::default();
let mut other: BTreeMap<
PackageId,
BTreeMap<
VersionString,
(
&Model<PackageVersionInfo>,
Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>,
),
>,
> = Default::default();
for (id, version, info, assets) in get_matching_models(&peek.as_index().as_package(), &params)?
{
let package_best = best.entry(id.clone()).or_default(); let package_best = best.entry(id.clone()).or_default();
let package_other = other.entry(id.clone()).or_default(); let package_other = other.entry(id.clone()).or_default();
if params if params
@@ -242,12 +218,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
package_other.insert(worse_version, info); package_other.insert(worse_version, info);
} }
} }
package_best.insert(version.into(), (info, assets)); package_best.insert(version.into(), info);
} else { } else {
package_other.insert(version.into(), (info, assets)); package_other.insert(version.into(), info);
} }
} }
let mut res = if let Some(id) = &params.id { if let Some(id) = &params.id {
let categories = peek let categories = peek
.as_index() .as_index()
.as_package() .as_package()
@@ -256,23 +232,14 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
.map(|p| p.as_categories().de()) .map(|p| p.as_categories().de())
.transpose()? .transpose()?
.unwrap_or_default(); .unwrap_or_default();
let best = best let best: BTreeMap<VersionString, PackageVersionInfo> = best
.remove(id) .remove(id)
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
.map(|(k, (i, a))| { .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.try_collect()?; .try_collect()?;
let other = other.remove(id).unwrap_or_default(); let other = other.remove(id).unwrap_or_default();
match params.other_versions { match params.other_versions.unwrap_or_default() {
PackageDetailLevel::None => to_value(&GetPackageResponse { PackageDetailLevel::None => to_value(&GetPackageResponse {
categories, categories,
best, best,
@@ -284,7 +251,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
other_versions: Some( other_versions: Some(
other other
.into_iter() .into_iter()
.map(|(k, (i, _))| from_value(i.as_value().clone()).map(|v| (k, v))) .map(|(k, i)| from_value(i.into()).map(|v| (k, v)))
.try_collect()?, .try_collect()?,
), ),
}), }),
@@ -293,21 +260,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
best, best,
other_versions: other other_versions: other
.into_iter() .into_iter()
.map(|(k, (i, a))| { .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.try_collect()?, .try_collect()?,
}), }),
} }
} else { } else {
match params.other_versions { match params.other_versions.unwrap_or_default() {
PackageDetailLevel::None => to_value( PackageDetailLevel::None => to_value(
&best &best
.into_iter() .into_iter()
@@ -326,9 +284,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories, categories,
best: best best: best
.into_iter() .into_iter()
.map(|(k, (i, _))| { .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
from_value(i.as_value().clone()).map(|v| (k, v))
})
.try_collect()?, .try_collect()?,
other_versions: None, other_versions: None,
}, },
@@ -355,24 +311,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories, categories,
best: best best: best
.into_iter() .into_iter()
.into_iter() .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.try_collect()?, .try_collect()?,
other_versions: Some( other_versions: Some(
other other
.into_iter() .into_iter()
.map(|(k, (i, _))| { .map(|(k, i)| from_value(i.into()).map(|v| (k, v)))
from_value(i.as_value().clone()).map(|v| (k, v))
})
.try_collect()?, .try_collect()?,
), ),
}, },
@@ -399,31 +343,11 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
categories, categories,
best: best best: best
.into_iter() .into_iter()
.into_iter() .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.try_collect()?, .try_collect()?,
other_versions: other other_versions: other
.into_iter() .into_iter()
.into_iter() .map(|(k, i)| Ok::<_, Error>((k, i.de()?)))
.map(|(k, (i, a))| {
Ok::<_, Error>((
k,
PackageVersionInfo {
metadata: i.as_metadata().de()?,
source_version: i.as_source_version().de()?,
s9pks: a,
},
))
})
.try_collect()?, .try_collect()?,
}, },
)) ))
@@ -431,47 +355,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu
.try_collect::<_, GetPackagesResponseFull, _>()?, .try_collect::<_, GetPackagesResponseFull, _>()?,
), ),
} }
}?;
// TODO: remove
if true
|| params.device_info.map_or(false, |d| {
"0.4.0-alpha.17"
.parse::<Version>()
.map_or(false, |v| d.os.version <= v)
})
{
let patch = |v: &mut Value| {
for kind in ["best", "otherVersions"] {
for (_, v) in v[kind]
.as_object_mut()
.into_iter()
.map(|v| v.iter_mut())
.flatten()
{
let Some(mut tup) = v["s9pks"].get(0).cloned() else {
continue;
};
v["s9pk"] = tup[1].take();
v["hardwareRequirements"] = tup[0].take();
v["s9pk"]["url"] = v["s9pk"]["urls"][0].clone();
}
}
};
if params.id.is_some() {
patch(&mut res);
} else {
for (_, v) in res
.as_object_mut()
.into_iter()
.map(|v| v.iter_mut())
.flatten()
{
patch(v);
}
}
} }
Ok(res)
} }
pub fn display_package_info( pub fn display_package_info(
@@ -483,7 +367,7 @@ pub fn display_package_info(
} }
if let Some(_) = params.rest.id { if let Some(_) = params.rest.id {
if params.rest.other_versions == PackageDetailLevel::Full { if params.rest.other_versions.unwrap_or_default() == PackageDetailLevel::Full {
for table in from_value::<GetPackageResponseFull>(info)?.tables() { for table in from_value::<GetPackageResponseFull>(info)?.tables() {
table.print_tty(false)?; table.print_tty(false)?;
println!(); println!();
@@ -495,7 +379,7 @@ pub fn display_package_info(
} }
} }
} else { } else {
if params.rest.other_versions == PackageDetailLevel::Full { if params.rest.other_versions.unwrap_or_default() == PackageDetailLevel::Full {
for (_, package) in from_value::<GetPackagesResponseFull>(info)? { for (_, package) in from_value::<GetPackagesResponseFull>(info)? {
for table in package.tables() { for table in package.tables() {
table.print_tty(false)?; table.print_tty(false)?;
@@ -620,7 +504,7 @@ pub async fn cli_download(
} else { } else {
"Devices" "Devices"
}, },
hw.device.iter().map(|d| &d.pattern_description).join(", ") hw.device.iter().map(|d| &d.description).join(", ")
) )
.unwrap(); .unwrap();
} }

View File

@@ -1,8 +1,10 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::u32;
use chrono::Utc; use chrono::Utc;
use exver::{Version, VersionRange}; use exver::{Version, VersionRange};
use imbl_value::InternedString; use imbl_value::InternedString;
use patch_db::ModelExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use url::Url; use url::Url;
@@ -223,50 +225,64 @@ impl PackageVersionInfo {
} }
} }
impl Model<PackageVersionInfo> { impl Model<PackageVersionInfo> {
pub fn for_device( /// Filters this package version for compatibility with the given device.
&self, /// Returns false if the package is incompatible (should be removed).
device_info: &DeviceInfo, /// Modifies s9pks in place to only include compatible variants.
) -> Result<Option<Vec<(HardwareRequirements, RegistryAsset<MerkleArchiveCommitment>)>>, Error> pub fn for_device(&mut self, device_info: &DeviceInfo) -> Result<bool, Error> {
{
if !self if !self
.as_metadata() .as_metadata()
.as_os_version() .as_os_version()
.de()? .de()?
.satisfies(&device_info.os.compat) .satisfies(&device_info.os.compat)
{ {
return Ok(None); return Ok(false);
} }
let mut s9pk = self.as_s9pks().de()?; if let Some(hw) = &device_info.hardware {
s9pk.retain(|(hw, _)| { self.as_s9pks_mut().mutate(|s9pks| {
if let Some(arch) = &hw.arch { s9pks.retain(|(hw_req, _)| {
if !arch.contains(&device_info.hardware.arch) { if let Some(arch) = &hw_req.arch {
return false; if !arch.contains(&hw.arch) {
return false;
}
}
if let Some(ram) = hw_req.ram {
if hw.ram < ram {
return false;
}
}
if let Some(dev) = &hw.devices {
for device_filter in &hw_req.device {
if !dev
.iter()
.filter(|d| d.class() == &*device_filter.class)
.any(|d| device_filter.matches(d))
{
return false;
}
}
}
true
});
if hw.devices.is_some() {
s9pks.sort_by_key(|(req, _)| req.specificity_desc());
} else {
s9pks.sort_by_key(|(req, _)| {
let (dev, arch, ram) = req.specificity_desc();
(u32::MAX - dev, arch, ram)
});
} }
} Ok(())
if let Some(ram) = hw.ram { })?;
if device_info.hardware.ram < ram {
return false;
}
}
for device_filter in &hw.device {
if !device_info
.hardware
.devices
.iter()
.filter(|d| d.class() == &*device_filter.class)
.any(|d| device_filter.pattern.as_ref().is_match(d.product()))
{
return false;
}
}
true
});
if s9pk.is_empty() { if ModelExt::as_value(self.as_s9pks())
Ok(None) .as_array()
} else { .map_or(true, |s| s.is_empty())
Ok(Some(s9pk)) {
return Ok(false);
}
} }
Ok(true)
} }
} }

View File

@@ -242,12 +242,13 @@ impl TryFrom<ManifestV1> for Manifest {
.device .device
.into_iter() .into_iter()
.map(|(class, product)| DeviceFilter { .map(|(class, product)| DeviceFilter {
pattern_description: format!( description: format!(
"a {class} device matching the expression {}", "a {class} device matching the expression {}",
product.as_ref() product.as_ref()
), ),
class, class,
pattern: product, product: Some(product),
..Default::default()
}) })
.collect(), .collect(),
}, },

View File

@@ -15,6 +15,7 @@ use crate::s9pk::git_hash::GitHash;
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::expected::{Expected, Filter}; use crate::s9pk::merkle_archive::expected::{Expected, Filter};
use crate::s9pk::v2::pack::ImageConfig; use crate::s9pk::v2::pack::ImageConfig;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::serde::Regex; use crate::util::serde::Regex;
use crate::util::{VersionString, mime}; use crate::util::{VersionString, mime};
use crate::version::{Current, VersionT}; use crate::version::{Current, VersionT};
@@ -189,21 +190,107 @@ impl HardwareRequirements {
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct DeviceFilter { pub struct DeviceFilter {
pub description: String,
#[ts(type = "\"processor\" | \"display\"")] #[ts(type = "\"processor\" | \"display\"")]
pub class: InternedString, pub class: InternedString,
#[ts(type = "string")] #[ts(type = "string | null")]
pub pattern: Regex, pub product: Option<Regex>,
pub pattern_description: String, #[ts(type = "string | null")]
pub vendor: Option<Regex>,
#[ts(optional)]
pub capabilities: Option<BTreeSet<InternedString>>,
#[ts(optional)]
pub driver: Option<InternedString>,
} }
// Omit description
impl PartialEq for DeviceFilter { impl PartialEq for DeviceFilter {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.class == other.class self.class == other.class
&& InternedString::from_display(self.pattern.as_ref()) && self.product == other.product
== InternedString::from_display(other.pattern.as_ref()) && self.vendor == other.vendor
&& self.capabilities == other.capabilities
&& self.driver == other.driver
}
}
impl DeviceFilter {
pub fn matches(&self, device: &LshwDevice) -> bool {
if &*self.class != device.class() {
return false;
}
match device {
LshwDevice::Processor(LshwProcessor {
product,
vendor,
capabilities,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
true
}
LshwDevice::Display(LshwDisplay {
product,
vendor,
capabilities,
driver,
}) => {
if let Some(match_product) = &self.product {
if !product
.as_deref()
.map_or(false, |p| match_product.as_ref().is_match(p))
{
return false;
}
}
if let Some(match_vendor) = &self.vendor {
if !vendor
.as_deref()
.map_or(false, |v| match_vendor.as_ref().is_match(v))
{
return false;
}
}
if !self
.capabilities
.as_ref()
.map_or(true, |c| c.is_subset(capabilities))
{
return false;
}
if !self
.driver
.as_ref()
.map_or(true, |d| Some(d) == driver.as_ref())
{
return false;
}
true
}
}
} }
} }

View File

@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use clap::Parser; use clap::Parser;
use imbl::OrdMap;
use imbl_value::Value; use imbl_value::Value;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
@@ -53,7 +54,13 @@ impl Context for ContainerCliContext {
} }
impl CallRemote<EffectContext> for ContainerCliContext { impl CallRemote<EffectContext> for ContainerCliContext {
async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result<Value, RpcError> { async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
call_remote_socket( call_remote_socket(
tokio::net::UnixStream::connect(&self.0.socket) tokio::net::UnixStream::connect(&self.0.socket)
.await .await

View File

@@ -9,7 +9,6 @@ use std::sync::{Arc, Weak};
use std::time::Duration; use std::time::Duration;
use axum::extract::ws::Utf8Bytes; use axum::extract::ws::Utf8Bytes;
use crate::util::net::WebSocket;
use clap::Parser; use clap::Parser;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use futures::stream::FusedStream; use futures::stream::FusedStream;
@@ -48,6 +47,7 @@ use crate::util::Never;
use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::future::NonDetachingJoinHandle; use crate::util::future::NonDetachingJoinHandle;
use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file}; use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
use crate::util::net::WebSocket;
use crate::util::serde::Pem; use crate::util::serde::Pem;
use crate::util::sync::SyncMutex; use crate::util::sync::SyncMutex;
use crate::volume::data_dir; use crate::volume::data_dir;

View File

@@ -51,6 +51,7 @@ impl Model<StatusInfo> {
} }
pub fn stopped(&mut self) -> Result<(), Error> { pub fn stopped(&mut self) -> Result<(), Error> {
self.as_started_mut().ser(&None)?; self.as_started_mut().ser(&None)?;
self.as_health_mut().ser(&Default::default())?;
Ok(()) Ok(())
} }
pub fn restart(&mut self) -> Result<(), Error> { pub fn restart(&mut self) -> Result<(), Error> {
@@ -59,7 +60,7 @@ impl Model<StatusInfo> {
Ok(()) Ok(())
} }
pub fn init(&mut self) -> Result<(), Error> { pub fn init(&mut self) -> Result<(), Error> {
self.as_started_mut().ser(&None)?; self.stopped()?;
self.as_desired_mut().map_mutate(|s| { self.as_desired_mut().map_mutate(|s| {
Ok(match s { Ok(match s {
DesiredStatus::BackingUp { DesiredStatus::BackingUp {

View File

@@ -251,6 +251,8 @@ impl CallRemote<TunnelContext> for CliContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value, params: Value,
_: Empty, _: Empty,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
@@ -315,6 +317,7 @@ impl CallRemote<TunnelContext, TunnelUrlParams> for RpcContext {
async fn call_remote( async fn call_remote(
&self, &self,
mut method: &str, mut method: &str,
_: OrdMap<&'static str, Value>,
params: Value, params: Value,
TunnelUrlParams { tunnel }: TunnelUrlParams, TunnelUrlParams { tunnel }: TunnelUrlParams,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {

View File

@@ -1,9 +1,12 @@
use std::collections::BTreeSet;
use imbl_value::InternedString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::process::Command; use tokio::process::Command;
use ts_rs::TS; use ts_rs::TS;
use crate::prelude::*;
use crate::util::Invoke; use crate::util::Invoke;
use crate::{Error, ResultExt};
const KNOWN_CLASSES: &[&str] = &["processor", "display"]; const KNOWN_CLASSES: &[&str] = &["processor", "display"];
@@ -22,22 +25,57 @@ impl LshwDevice {
Self::Display(_) => "display", Self::Display(_) => "display",
} }
} }
pub fn product(&self) -> &str { pub fn from_value(value: &Value) -> Option<Self> {
match self { match value["class"].as_str() {
Self::Processor(hw) => hw.product.as_str(), Some("processor") => Some(LshwDevice::Processor(LshwProcessor::from_value(value))),
Self::Display(hw) => hw.product.as_str(), Some("display") => Some(LshwDevice::Display(LshwDisplay::from_value(value))),
_ => None,
} }
} }
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwProcessor { pub struct LshwProcessor {
pub product: String, pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
}
impl LshwProcessor {
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),
vendor: value["vendor"].as_str().map(From::from),
capabilities: value["capabilities"]
.as_object()
.into_iter()
.flat_map(|o| o.keys())
.map(|k| k.clone())
.collect(),
}
}
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
pub struct LshwDisplay { pub struct LshwDisplay {
pub product: String, pub product: Option<InternedString>,
pub vendor: Option<InternedString>,
pub capabilities: BTreeSet<InternedString>,
pub driver: Option<InternedString>,
}
impl LshwDisplay {
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),
vendor: value["vendor"].as_str().map(From::from),
capabilities: value["capabilities"]
.as_object()
.into_iter()
.flat_map(|o| o.keys())
.map(|k| k.clone())
.collect(),
driver: value["configuration"]["driver"].as_str().map(From::from),
}
}
} }
pub async fn lshw() -> Result<Vec<LshwDevice>, Error> { pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
@@ -47,19 +85,10 @@ pub async fn lshw() -> Result<Vec<LshwDevice>, Error> {
cmd.arg("-class").arg(*class); cmd.arg("-class").arg(*class);
} }
Ok( Ok(
serde_json::from_slice::<Vec<serde_json::Value>>( serde_json::from_slice::<Vec<Value>>(&cmd.invoke(crate::ErrorKind::Lshw).await?)
&cmd.invoke(crate::ErrorKind::Lshw).await?, .with_kind(crate::ErrorKind::Deserialization)?
) .iter()
.with_kind(crate::ErrorKind::Deserialization)? .filter_map(LshwDevice::from_value)
.into_iter() .collect(),
.filter_map(|v| match serde_json::from_value(v) {
Ok(a) => Some(a),
Err(e) => {
tracing::error!("Failed to parse lshw output: {e}");
tracing::debug!("{e:?}");
None
}
})
.collect(),
) )
} }

View File

@@ -1127,6 +1127,11 @@ impl Serialize for Regex {
serialize_display(&self.0, serializer) serialize_display(&self.0, serializer)
} }
} }
impl PartialEq for Regex {
fn eq(&self, other: &Self) -> bool {
InternedString::from_display(self.as_ref()) == InternedString::from_display(other.as_ref())
}
}
// TODO: make this not allocate // TODO: make this not allocate
#[derive(Debug)] #[derive(Debug)]

View File

@@ -1,6 +1,7 @@
use std::path::Path; use std::path::Path;
use exver::{PreReleaseSegment, VersionRange}; use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use tokio::fs::File; use tokio::fs::File;
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
@@ -10,7 +11,7 @@ use crate::context::RpcContext;
use crate::install::PKG_ARCHIVE_DIR; use crate::install::PKG_ARCHIVE_DIR;
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::s9pk::manifest::{DeviceFilter, Manifest}; use crate::s9pk::manifest::Manifest;
use crate::s9pk::merkle_archive::MerkleArchive; use crate::s9pk::merkle_archive::MerkleArchive;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::SIG_CONTEXT;
@@ -84,28 +85,8 @@ impl VersionT for Version {
let mut manifest = previous_manifest.clone(); let mut manifest = previous_manifest.clone();
if let Some(device) = if let Some(_) = previous_manifest["hardwareRequirements"]["device"].as_object() {
previous_manifest["hardwareRequirements"]["device"].as_object() manifest["hardwareRequirements"]["device"] = json!([]);
{
manifest["hardwareRequirements"]["device"] = to_value(
&device
.into_iter()
.map(|(class, product)| {
Ok::<_, Error>(DeviceFilter {
pattern_description: format!(
"a {class} device matching the expression {}",
&product
),
class: class.clone(),
pattern: from_value(product.clone())?,
})
})
.fold(Ok::<_, Error>(Vec::new()), |acc, value| {
let mut acc = acc?;
acc.push(value?);
Ok(acc)
})?,
)?;
} }
if previous_manifest != manifest { if previous_manifest != manifest {

View File

@@ -29,34 +29,6 @@ if [ -f /etc/default/grub ]; then
else else
echo 'GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' >> /etc/default/grub echo 'GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' >> /etc/default/grub
fi fi
cat > /etc/grub.d/01_reboot_efi <<-EOF
#!/bin/sh
set -e
# Only affect EFI systems
if [ ! -d /sys/firmware/efi ]; then
exit 0
fi
# Import helpers (path is Debian/Ubuntu style)
. /usr/lib/grub/grub-mkconfig_lib
# Append reboot=efi to GRUB_CMDLINE_LINUX* seen by later scripts
if [ -n "\${GRUB_CMDLINE_LINUX}" ]; then
GRUB_CMDLINE_LINUX="\${GRUB_CMDLINE_LINUX} reboot=efi"
else
GRUB_CMDLINE_LINUX="reboot=efi"
fi
if [ -n "\${GRUB_CMDLINE_LINUX_DEFAULT}" ]; then
GRUB_CMDLINE_LINUX_DEFAULT="\${GRUB_CMDLINE_LINUX_DEFAULT} reboot=efi"
fi
export GRUB_CMDLINE_LINUX GRUB_CMDLINE_LINUX_DEFAULT
EOF
chmod +x /etc/grub.d/01_reboot_efi
fi fi
VERSION="$(cat /usr/lib/startos/VERSION.txt)" VERSION="$(cat /usr/lib/startos/VERSION.txt)"

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnySignature } from "./AnySignature"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export type AddMirrorParams = {
url: string
commitment: MerkleArchiveCommitment
signature: AnySignature
}

View File

@@ -3,7 +3,7 @@ import type { AnySignature } from "./AnySignature"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export type AddPackageParams = { export type AddPackageParams = {
url: string urls: string[]
commitment: MerkleArchiveCommitment commitment: MerkleArchiveCommitment
signature: AnySignature signature: AnySignature
} }

View File

@@ -1,7 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DeviceFilter = { export type DeviceFilter = {
description: string
class: "processor" | "display" class: "processor" | "display"
pattern: string product: string | null
patternDescription: string vendor: string | null
capabilities?: Array<string>
driver?: string
} }

View File

@@ -7,5 +7,5 @@ export type GetPackageParams = {
id: PackageId | null id: PackageId | null
targetVersion: string | null targetVersion: string | null
sourceVersion: Version | null sourceVersion: Version | null
otherVersions: PackageDetailLevel otherVersions: PackageDetailLevel | null
} }

View File

@@ -1,3 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwDisplay = { product: string } export type LshwDisplay = {
product: string | null
vendor: string | null
capabilities: Array<string>
driver: string | null
}

View File

@@ -1,3 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwProcessor = { product: string } export type LshwProcessor = {
product: string | null
vendor: string | null
capabilities: Array<string>
}

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from "./PackageId"
import type { Version } from "./Version"
export type RemoveMirrorParams = {
id: PackageId
version: Version
url: string
}

View File

@@ -1,5 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Base64 } from "./Base64"
import type { PackageId } from "./PackageId" import type { PackageId } from "./PackageId"
import type { Version } from "./Version" import type { Version } from "./Version"
export type RemovePackageParams = { id: PackageId; version: Version } export type RemovePackageParams = {
id: PackageId
version: Version
sighash: Base64 | null
}

View File

@@ -13,6 +13,7 @@ export { ActionVisibility } from "./ActionVisibility"
export { AddAdminParams } from "./AddAdminParams" export { AddAdminParams } from "./AddAdminParams"
export { AddAssetParams } from "./AddAssetParams" export { AddAssetParams } from "./AddAssetParams"
export { AddCategoryParams } from "./AddCategoryParams" export { AddCategoryParams } from "./AddCategoryParams"
export { AddMirrorParams } from "./AddMirrorParams"
export { AddPackageParams } from "./AddPackageParams" export { AddPackageParams } from "./AddPackageParams"
export { AddPackageSignerParams } from "./AddPackageSignerParams" export { AddPackageSignerParams } from "./AddPackageSignerParams"
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams" export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
@@ -171,6 +172,7 @@ export { RegistryInfo } from "./RegistryInfo"
export { RemoveAdminParams } from "./RemoveAdminParams" export { RemoveAdminParams } from "./RemoveAdminParams"
export { RemoveAssetParams } from "./RemoveAssetParams" export { RemoveAssetParams } from "./RemoveAssetParams"
export { RemoveCategoryParams } from "./RemoveCategoryParams" export { RemoveCategoryParams } from "./RemoveCategoryParams"
export { RemoveMirrorParams } from "./RemoveMirrorParams"
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams" export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
export { RemovePackageParams } from "./RemovePackageParams" export { RemovePackageParams } from "./RemovePackageParams"
export { RemovePackageSignerParams } from "./RemovePackageSignerParams" export { RemovePackageSignerParams } from "./RemovePackageSignerParams"