mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 21:30:41 +00:00
Add version parsing, FMRI formatting, and license content resolution
- Enhanced `get_info` handler to parse version components into `Build Release`, `Branch`, and `Packaging Date`. - Added proper FMRI formatting in `pkg://<publisher>/<name>@<version>` format. - Implemented license content resolution with file-based lookup, gzip decompression, and content truncation. - Introduced compressed and uncompressed package size calculations for manifest actions. - Updated dependencies to include `chrono` and `flate2` for date parsing and gzip decompression.
This commit is contained in:
parent
cff3d5d960
commit
3457b4acba
4 changed files with 193 additions and 7 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2104,8 +2104,10 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"flate2",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"knuffel",
|
"knuffel",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ serde_json = "1.0"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
nix = { version = "0.30", features = ["signal", "process", "user", "fs"] }
|
nix = { version = "0.30", features = ["signal", "process", "user", "fs"] }
|
||||||
sha1 = "0.10"
|
sha1 = "0.10"
|
||||||
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||||
|
flate2 = "1"
|
||||||
|
|
||||||
# Telemetry
|
# Telemetry
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ use crate::errors::DepotError;
|
||||||
use libips::fmri::Fmri;
|
use libips::fmri::Fmri;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use libips::actions::Manifest;
|
use libips::actions::Manifest;
|
||||||
|
use chrono::{NaiveDateTime, Utc, TimeZone, Datelike, Timelike};
|
||||||
|
use libips::actions::Property;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Read as _;
|
||||||
|
|
||||||
pub async fn get_info(
|
pub async fn get_info(
|
||||||
State(repo): State<Arc<DepotRepo>>,
|
State(repo): State<Arc<DepotRepo>>,
|
||||||
|
|
@ -30,17 +34,85 @@ pub async fn get_info(
|
||||||
out.push_str(&format!("Summary: {}\n", summary));
|
out.push_str(&format!("Summary: {}\n", summary));
|
||||||
}
|
}
|
||||||
out.push_str(&format!("Publisher: {}\n", publisher));
|
out.push_str(&format!("Publisher: {}\n", publisher));
|
||||||
out.push_str(&format!("Version: {}\n", fmri.version()));
|
// Parse version components for Version, Build Release, Branch, and Packaging Date
|
||||||
out.push_str(&format!("FMRI: pkg://{}/{}\n", publisher, fmri));
|
let version_full = fmri.version();
|
||||||
|
let mut version_core = version_full.clone();
|
||||||
|
let mut build_release: Option<String> = None;
|
||||||
|
let mut branch: Option<String> = None;
|
||||||
|
let mut ts_str: Option<String> = None;
|
||||||
|
|
||||||
|
if let Some((core, rest)) = version_full.split_once(',') {
|
||||||
|
version_core = core.to_string();
|
||||||
|
if let Some((rel_branch, ts)) = rest.split_once(':') {
|
||||||
|
ts_str = Some(ts.to_string());
|
||||||
|
if let Some((rel, br)) = rel_branch.split_once('-') {
|
||||||
|
if !rel.is_empty() { build_release = Some(rel.to_string()); }
|
||||||
|
if !br.is_empty() { branch = Some(br.to_string()); }
|
||||||
|
} else {
|
||||||
|
// No branch
|
||||||
|
if !rel_branch.is_empty() { build_release = Some(rel_branch.to_string()); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No timestamp
|
||||||
|
if let Some((rel, br)) = rest.split_once('-') {
|
||||||
|
if !rel.is_empty() { build_release = Some(rel.to_string()); }
|
||||||
|
if !br.is_empty() { branch = Some(br.to_string()); }
|
||||||
|
} else if !rest.is_empty() {
|
||||||
|
build_release = Some(rest.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str(&format!("Version: {}\n", version_core));
|
||||||
|
if let Some(rel) = build_release { out.push_str(&format!("Build Release: {}\n", rel)); }
|
||||||
|
if let Some(br) = branch { out.push_str(&format!("Branch: {}\n", br)); }
|
||||||
|
if let Some(ts) = ts_str.and_then(|s| format_packaging_date(&s)) {
|
||||||
|
out.push_str(&format!("Packaging Date: {}\n", ts));
|
||||||
|
}
|
||||||
|
// Compute sizes from manifest file actions (pkg.size and pkg.csize)
|
||||||
|
let (total_size, total_csize) = compute_sizes(&manifest);
|
||||||
|
out.push_str(&format!("Size: {}\n", human_bytes(total_size)));
|
||||||
|
out.push_str(&format!("Compressed Size: {}\n", human_bytes(total_csize)));
|
||||||
|
// Construct a correct FMRI string:
|
||||||
|
// Always use the publisher from the URL segment, and format as
|
||||||
|
// pkg://<publisher>/<name>@<version>
|
||||||
|
let name = fmri.stem();
|
||||||
|
let version = fmri.version();
|
||||||
|
if version.is_empty() {
|
||||||
|
out.push_str(&format!("FMRI: pkg://{}/{}\n", publisher, name));
|
||||||
|
} else {
|
||||||
|
out.push_str(&format!("FMRI: pkg://{}/{}@{}\n", publisher, name, version));
|
||||||
|
}
|
||||||
|
|
||||||
// License
|
// License
|
||||||
// License might be an action (License action) or attribute.
|
// Print actual license text content from repository instead of hash.
|
||||||
// Usually it's license actions.
|
|
||||||
// For M2 minimal parity, we can skip detailed license text or just say empty if not found.
|
|
||||||
// depot.txt sample shows "License:" empty line if none?
|
|
||||||
out.push_str("\nLicense:\n");
|
out.push_str("\nLicense:\n");
|
||||||
|
let mut first = true;
|
||||||
for license in &manifest.licenses {
|
for license in &manifest.licenses {
|
||||||
out.push_str(&format!("{}\n", license.payload));
|
if !first { out.push('\n'); }
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
// Optional license name header for readability
|
||||||
|
if let Some(name_prop) = license.properties.get("license") {
|
||||||
|
if !name_prop.value.is_empty() {
|
||||||
|
out.push_str(&format!("Name: {}\n", name_prop.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve file by digest payload
|
||||||
|
let digest = license.payload.trim();
|
||||||
|
if !digest.is_empty() {
|
||||||
|
match resolve_license_text(&repo, &publisher, digest) {
|
||||||
|
Some(text) => {
|
||||||
|
out.push_str(&text);
|
||||||
|
if !text.ends_with('\n') { out.push('\n'); }
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Fallback: show the digest if content could not be resolved
|
||||||
|
out.push_str(&format!("<license content unavailable for digest {}>\n", digest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
|
@ -49,6 +121,43 @@ pub async fn get_info(
|
||||||
).into_response())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to read and decode the license text for a given digest from the repository.
|
||||||
|
// - Prefer publisher-scoped file path; fallback to global location.
|
||||||
|
// - If content appears to be gzip-compressed (magic 1f 8b), decompress.
|
||||||
|
// - Decode as UTF-8 (lossy) and enforce a maximum output size to avoid huge responses.
|
||||||
|
fn resolve_license_text(repo: &DepotRepo, publisher: &str, digest: &str) -> Option<String> {
|
||||||
|
let path = repo.get_file_path(publisher, digest)?;
|
||||||
|
let bytes = fs::read(&path).ok()?;
|
||||||
|
|
||||||
|
// Detect gzip magic
|
||||||
|
let mut data: Vec<u8> = bytes;
|
||||||
|
if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
|
||||||
|
// Attempt to gunzip
|
||||||
|
let mut decoder = flate2::read::GzDecoder::new(&data[..]);
|
||||||
|
let mut decompressed = Vec::new();
|
||||||
|
match decoder.read_to_end(&mut decompressed) {
|
||||||
|
Ok(_) => data = decompressed,
|
||||||
|
Err(_) => {
|
||||||
|
// Leave as-is if decompression fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit output size to 256 KiB
|
||||||
|
const MAX_LICENSE_BYTES: usize = 256 * 1024;
|
||||||
|
let truncated = data.len() > MAX_LICENSE_BYTES;
|
||||||
|
if truncated {
|
||||||
|
data.truncate(MAX_LICENSE_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text = String::from_utf8_lossy(&data).to_string();
|
||||||
|
if truncated {
|
||||||
|
if !text.ends_with('\n') { text.push('\n'); }
|
||||||
|
text.push_str("...[truncated]\n");
|
||||||
|
}
|
||||||
|
Some(text)
|
||||||
|
}
|
||||||
|
|
||||||
fn find_attr(manifest: &Manifest, key: &str) -> Option<String> {
|
fn find_attr(manifest: &Manifest, key: &str) -> Option<String> {
|
||||||
for attr in &manifest.attributes {
|
for attr in &manifest.attributes {
|
||||||
if attr.key == key {
|
if attr.key == key {
|
||||||
|
|
@ -57,3 +166,74 @@ fn find_attr(manifest: &Manifest, key: &str) -> Option<String> {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn month_name(month: u32) -> &'static str {
|
||||||
|
match month {
|
||||||
|
1 => "January",
|
||||||
|
2 => "February",
|
||||||
|
3 => "March",
|
||||||
|
4 => "April",
|
||||||
|
5 => "May",
|
||||||
|
6 => "June",
|
||||||
|
7 => "July",
|
||||||
|
8 => "August",
|
||||||
|
9 => "September",
|
||||||
|
10 => "October",
|
||||||
|
11 => "November",
|
||||||
|
12 => "December",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_packaging_date(ts: &str) -> Option<String> {
|
||||||
|
// Expect formats like YYYYMMDDThhmmssZ or with fractional seconds before Z
|
||||||
|
let clean_ts = if let Some((base, _frac)) = ts.split_once('.') { format!("{}Z", base) } else { ts.to_string() };
|
||||||
|
let ndt = NaiveDateTime::parse_from_str(&clean_ts, "%Y%m%dT%H%M%SZ").ok()?;
|
||||||
|
let dt_utc = Utc.from_utc_datetime(&ndt);
|
||||||
|
let month = month_name(dt_utc.month() as u32);
|
||||||
|
let day = dt_utc.day();
|
||||||
|
let year = dt_utc.year();
|
||||||
|
let hour24 = dt_utc.hour();
|
||||||
|
let (ampm, hour12) = if hour24 == 0 { ("AM", 12) } else if hour24 < 12 { ("AM", hour24) } else if hour24 == 12 { ("PM", 12) } else { ("PM", hour24 - 12) };
|
||||||
|
let minute = dt_utc.minute();
|
||||||
|
let second = dt_utc.second();
|
||||||
|
Some(format!("{} {:02}, {} at {:02}:{:02}:{:02} {}", month, day, year, hour12, minute, second, ampm))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum pkg.size (uncompressed) and pkg.csize (compressed) over all file actions
|
||||||
|
fn compute_sizes(manifest: &Manifest) -> (u128, u128) {
|
||||||
|
let mut size: u128 = 0;
|
||||||
|
let mut csize: u128 = 0;
|
||||||
|
|
||||||
|
for file in &manifest.files {
|
||||||
|
for Property { key, value } in &file.properties {
|
||||||
|
if key == "pkg.size" {
|
||||||
|
if let Ok(v) = value.parse::<u128>() { size = size.saturating_add(v); }
|
||||||
|
} else if key == "pkg.csize" {
|
||||||
|
if let Ok(v) = value.parse::<u128>() { csize = csize.saturating_add(v); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(size, csize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn human_bytes(bytes: u128) -> String {
|
||||||
|
// Use binary (IEC-like) units for familiarity; format with two decimals for KB and above
|
||||||
|
const KIB: u128 = 1024;
|
||||||
|
const MIB: u128 = 1024 * 1024;
|
||||||
|
const GIB: u128 = 1024 * 1024 * 1024;
|
||||||
|
const TIB: u128 = 1024 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
if bytes < KIB {
|
||||||
|
return format!("{} B", bytes);
|
||||||
|
} else if bytes < MIB {
|
||||||
|
return format!("{:.2} KB", (bytes as f64) / (KIB as f64));
|
||||||
|
} else if bytes < GIB {
|
||||||
|
return format!("{:.2} MB", (bytes as f64) / (MIB as f64));
|
||||||
|
} else if bytes < TIB {
|
||||||
|
return format!("{:.2} GB", (bytes as f64) / (GIB as f64));
|
||||||
|
} else {
|
||||||
|
return format!("{:.2} TB", (bytes as f64) / (TIB as f64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@ async fn test_depot_server() {
|
||||||
let info_text = resp.text().await.unwrap();
|
let info_text = resp.text().await.unwrap();
|
||||||
assert!(info_text.contains("Name: example"));
|
assert!(info_text.contains("Name: example"));
|
||||||
assert!(info_text.contains("Summary: Test Package"));
|
assert!(info_text.contains("Summary: Test Package"));
|
||||||
|
// Ensure FMRI format is correct: pkg://<publisher>/<name>@<version>
|
||||||
|
assert!(info_text.contains("FMRI: pkg://test/example@1.0.0"), "Info FMRI was: {}", info_text);
|
||||||
|
|
||||||
// 5. Test Publisher v1
|
// 5. Test Publisher v1
|
||||||
let pub_url = format!("{}/test/publisher/1", base_url);
|
let pub_url = format!("{}/test/publisher/1", base_url);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue