diff --git a/Cargo.toml b/Cargo.toml index b62a0b2..581b473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ pedantic = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 } unwrap_used = "deny" missing_errors_doc = "allow" +missing_panics_doc = "allow" [profile.release] strip = true diff --git a/crates/minapk/Cargo.toml b/crates/minapk/Cargo.toml index 068a635..7e6dc43 100644 --- a/crates/minapk/Cargo.toml +++ b/crates/minapk/Cargo.toml @@ -3,3 +3,5 @@ name = "minapk" version = "0.1.0" edition = "2024" +[lints] +workspace = true diff --git a/crates/minapk/src/index.rs b/crates/minapk/src/index.rs new file mode 100644 index 0000000..e1ab71c --- /dev/null +++ b/crates/minapk/src/index.rs @@ -0,0 +1,124 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, + usize, +}; + +use crate::shell::{sh_out, sh_status}; +use crate::{CACHE_DIR, Cfg}; + +const INDEX_TTL_SECS: u64 = 86_400; + +#[must_use] +pub fn cache_index_path(cfg: &Cfg) -> PathBuf { + PathBuf::from(CACHE_DIR).join(format!( + "APKINDEX-{}-{}-{}.txt", + cfg.branch, cfg.repo, cfg.arch + )) +} +#[must_use] +pub fn repo_base(cfg: &Cfg) -> String { + format!( + "{}/alpine/{}/{}/{}/", + cfg.mirror.trim_end_matches('/'), + cfg.branch, + cfg.repo, + cfg.arch + ) +} +#[must_use] +pub fn index_stale(cfg: &Cfg) -> bool { + let p = cache_index_path(cfg); + fs::metadata(&p) + .and_then(|m| m.modified()) + .and_then(|t| { + t.elapsed() + .map_err(|_| io::Error::from(io::ErrorKind::Other)) + }) + .map_or(true, |age| age.as_secs() > INDEX_TTL_SECS) +} +pub fn index_refresh(cfg: &Cfg) -> Result<(), String> { + if !index_stale(cfg) { + return Ok(()); + } + let url = format!("{}APKINDEX.tar.gz", repo_base(cfg)); + println!("Downloading index..."); + let txt = sh_out(r#"curl -fsSL "$1" | tar -xzO APKINDEX"#, &[&url])?; + if txt.is_empty() { + return Err("APKINDEX empty".into()); + } + fs::create_dir_all(CACHE_DIR).map_err(|e| format!("mkdir cache: {e}"))?; + fs::write(cache_index_path(cfg), txt).map_err(|e| format!("write index: {e}"))?; + Ok(()) +} +pub fn index_ensure(cfg: &Cfg) -> Result<(), String> { + let p = cache_index_path(cfg); + if p.exists() { + println!("Index already cached"); + return Ok(()); + } + index_refresh(cfg) +} + +pub fn fetch_package(cfg: &Cfg, pkg_name: &str, version: &str) -> Result { + let url = format!("{}{}", repo_base(cfg), pkg_name); + let dest = tmp_apk(pkg_name, version); + let dest_str = dest.to_str().expect("Should be a valid dest string"); + println!("Downloading {url} to {dest_str}..."); + sh_status(r#"curl -fsSL "$1" -o "$2""#, &[&url, dest_str])?; + Ok(dest) +} + +#[must_use] +/// Return: (`file_name`, `version`, `download_size`, `install_size`) +pub fn resolve_package(cfg: &Cfg, wanted_pkg: &str) -> Option<(String, String, usize, usize)> { + let index = fs::read_to_string(cache_index_path(cfg)).ok()?; + let mut name = ""; + let mut version = ""; + let mut download_size = ""; + let mut install_size = ""; + + for line in index.lines().map(str::trim_end) { + if line.is_empty() { + if name == wanted_pkg { + return Some(( + format!("{name}-{version}.apk"), + version.to_string(), + download_size.parse().ok()?, + install_size.parse().ok()?, + )); + } + name = ""; + version = ""; + download_size = ""; + install_size = ""; + continue; + } + + if let Some(x) = line.strip_prefix("P:") { + name = x.trim(); + } else if let Some(x) = line.strip_prefix("V:") { + version = x.trim(); + } else if let Some(x) = line.strip_prefix("S:") { + download_size = x.trim(); + } else if let Some(x) = line.strip_prefix("I:") { + install_size = x.trim(); + } + } + None +} + +#[must_use] +pub fn tmp_apk(name: &str, ver: &str) -> PathBuf { + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + Path::new("/tmp").join(format!( + "minapk-{}-{}-{}.apk", + name, + ver.replace('/', "_"), + ts + )) +} diff --git a/crates/minapk/src/lib.rs b/crates/minapk/src/lib.rs new file mode 100644 index 0000000..b809350 --- /dev/null +++ b/crates/minapk/src/lib.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +pub const MINAPK_DATA: &str = "/var/lib/minapk"; +pub const CACHE_DIR: &str = "/var/cache/minapk"; + +#[derive(Clone)] +pub struct Cfg { + pub root: PathBuf, + pub name: Option, + pub arch: String, + pub branch: String, + pub repo: String, + pub mirror: String, +} + +impl Default for Cfg { + fn default() -> Self { + Self { + root: PathBuf::from("/"), + name: None, + arch: "riscv64".into(), + branch: "edge".into(), + repo: "main".into(), + mirror: "https://dl-cdn.alpinelinux.org".into(), + } + } +} + +pub mod index; +pub mod manifest; +pub mod shell; diff --git a/crates/minapk/src/main.rs b/crates/minapk/src/main.rs index 7b77849..fa0105d 100644 --- a/crates/minapk/src/main.rs +++ b/crates/minapk/src/main.rs @@ -5,39 +5,16 @@ // Defaults: --arch riscv64 --branch edge --repo main --mirror https://dl-cdn.alpinelinux.org // Notes: Network only for APKINDEX when stale or on package download. -use std::collections::BTreeMap; -use std::env; -use std::fs::{self, File}; -use std::io::{self, BufRead, BufReader, BufWriter, Write}; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::{SystemTime, UNIX_EPOCH}; - -const MANIFEST_PATH: &str = "var/lib/minapk/manifest"; -const CACHE_DIR: &str = "/var/cache/minapk"; -const INDEX_TTL_SECS: u64 = 86_400; - -#[derive(Clone)] -struct Cfg { - root: PathBuf, - name: Option, - arch: String, - branch: String, - repo: String, - mirror: String, -} -impl Default for Cfg { - fn default() -> Self { - Self { - root: PathBuf::from("/"), - name: None, - arch: "riscv64".into(), - branch: "edge".into(), - repo: "main".into(), - mirror: "https://dl-cdn.alpinelinux.org".into(), - } - } -} +use minapk::{ + Cfg, + index::{fetch_package, index_ensure, index_refresh, resolve_package}, + manifest::{manifest_load, manifest_save}, + shell::{extract_filtered, space_left_on}, +}; +use std::{ + env, fs, io, + path::{Path, PathBuf}, +}; fn main() { if let Err(e) = run() { @@ -55,30 +32,33 @@ fn run() -> Result<(), String> { match cmd.as_str() { "add" => { - let t = pos.last().ok_or("usage: add <.apk|name>")?.clone(); - let p = Path::new(&t); - if p.exists() || t.ends_with(".apk") { + let to_install = pos.last().ok_or("usage: add <.apk|name>")?.clone(); + let p = Path::new(&to_install); + if p.exists() + || std::path::Path::new(&to_install) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("apk")) + { add(&root, p, cfg.name) } else { - // by name - let name = t; - let m = manifest_load(&root)?; + // treat it as pkg name + let name = to_install; + let m = manifest_load()?; if m.contains_key(&name) { - println!("installed {name}"); + println!("{name} is already installed"); return Ok(()); } index_ensure(&cfg)?; - let apk = resolve_and_fetch(&cfg, &name)?; - let res = add(&root, &apk, Some(name)); - let _ = fs::remove_file(&apk); - res + check_and_install(&cfg, &name, &root)?; + println!("Installed {name}"); + Ok(()) } } "remove" => { let name = pos.first().ok_or("usage: remove ")?; remove(&root, name) } - "list" => list(&root), + "list" => list(), "update" => { let what = pos.first().ok_or("usage: update ")?; if *what == "index" { @@ -87,22 +67,55 @@ fn run() -> Result<(), String> { return Ok(()); } index_refresh(&cfg)?; - let m = manifest_load(&root)?; + let m = manifest_load()?; if m.contains_key(what) { drop(m); remove(&root, what)?; } - let apk = resolve_and_fetch(&cfg, what)?; - let res = add(&root, &apk, Some(what.clone())); - let _ = fs::remove_file(&apk); - println!("updated"); - res + + check_and_install(&cfg, what, &root)?; + println!("Updated {what}"); + Ok(()) } _ => Err(usage()), } } -// ----- CLI ----- +fn check_and_install(cfg: &Cfg, name: &str, root: &PathBuf) -> Result<(), String> { + let Some((file_name, version, download_size, install_size)) = resolve_package(cfg, name) else { + return Err(format!("Package {name} not found in index")); + }; + + println!("You are going to install {name} version {version}"); + + let availabe_space = space_left_on(root)?; + if download_size > availabe_space { + return Err(format!( + "Cannot download apk archive not enough space available on {}:\n available:{availabe_space}b < required:{download_size}b", + root.to_string_lossy() + )); + } + println!( + "Unfilterd install size is {install_size}b.\n Are you sure you want to continue? [y/N]:" + ); + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .map_err(|_| "Could not read response")?; + let lower_ans = input.to_lowercase(); + let trimed_ans = lower_ans.trim(); + match trimed_ans { + "y" | "yes" | "o" => {} + _ => { + return Ok(()); + } + } + + let apk = fetch_package(cfg, name, &version)?; + let res = add(root, &apk, Some(name.to_string())); + let _ = fs::remove_file(&apk); + res +} fn parse_args(argv: &[String]) -> Result<(Cfg, String, Vec), String> { let mut cfg = Cfg::default(); @@ -142,11 +155,13 @@ fn parse_args(argv: &[String]) -> Result<(Cfg, String, Vec), String> { let pos = argv.iter().skip(i + 1).cloned().collect(); Ok((cfg, cmd, pos)) } + fn req<'a>(argv: &'a [String], i: usize, f: &str) -> Result<&'a str, String> { argv.get(i) - .map(|s| s.as_str()) + .map(std::string::String::as_str) .ok_or_else(|| format!("{f} requires a value")) } + fn usage() -> String { "usage: minapk [--root DIR] [--name NAME] [--arch ARCH] [--branch BRANCH] [--repo REPO] [--mirror URL] add <.apk|name> @@ -155,205 +170,6 @@ fn usage() -> String { minapk [--root DIR] [--arch ARCH] [--branch BRANCH] [--repo REPO] [--mirror URL] update ".into() } -// ----- Manifest ----- - -fn manifest_path(root: &Path) -> Result { - let p = root.join(MANIFEST_PATH); - if let Some(d) = p.parent() { - fs::create_dir_all(d).map_err(|e| format!("mkdir {}: {e}", d.display()))?; - } - Ok(p) -} -fn manifest_load(root: &Path) -> Result>, String> { - let p = manifest_path(root)?; - let f = match File::open(&p) { - Ok(f) => f, - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(BTreeMap::new()), - Err(e) => return Err(format!("read {}: {e}", p.display())), - }; - let mut out: BTreeMap> = BTreeMap::new(); - let mut cur: Option = None; - let mut files: Vec = Vec::new(); - for line in BufReader::new(f).lines() { - let line = line.map_err(|e| format!("manifest read: {e}"))?; - if line.trim().is_empty() { - if let Some(n) = cur.take() { - out.insert(n, std::mem::take(&mut files)); - } - continue; - } - if cur.is_none() { - cur = Some(line.trim().into()); - } else { - files.push(line.trim().into()); - } - } - if let Some(n) = cur { - out.insert(n, files); - } - Ok(out) -} -fn manifest_save(root: &Path, m: &BTreeMap>) -> Result<(), String> { - let p = manifest_path(root)?; - let mut w = - BufWriter::new(File::create(&p).map_err(|e| format!("write {}: {e}", p.display()))?); - for (n, files) in m { - writeln!(w, "{n}").map_err(|e| format!("manifest write: {e}"))?; - for f in files { - writeln!(w, "{f}").map_err(|e| format!("manifest write: {e}"))?; - } - writeln!(w).map_err(|e| format!("manifest write: {e}"))?; - } - w.flush().map_err(|e| format!("manifest flush: {e}")) -} - -// ----- Index cache + resolver ----- - -fn cache_index_path(cfg: &Cfg) -> PathBuf { - PathBuf::from(CACHE_DIR).join(format!( - "APKINDEX-{}-{}-{}.txt", - cfg.branch, cfg.repo, cfg.arch - )) -} -fn repo_base(cfg: &Cfg) -> String { - format!( - "{}/alpine/{}/{}/{}/", - cfg.mirror.trim_end_matches('/'), - cfg.branch, - cfg.repo, - cfg.arch - ) -} -fn index_stale(cfg: &Cfg) -> bool { - let p = cache_index_path(cfg); - match fs::metadata(&p).and_then(|m| m.modified()).and_then(|t| { - t.elapsed() - .map_err(|_| io::Error::from(io::ErrorKind::Other)) - }) { - Ok(age) => age.as_secs() > INDEX_TTL_SECS, - Err(_) => true, - } -} -fn index_refresh(cfg: &Cfg) -> Result<(), String> { - if !index_stale(cfg) { - return Ok(()); - } - let url = format!("{}APKINDEX.tar.gz", repo_base(cfg)); - println!("Downloading index..."); - let txt = sh_out(r#"curl -fsSL "$1" | tar -xzO APKINDEX"#, &[&url])?; - if txt.is_empty() { - return Err("APKINDEX empty".into()); - } - fs::create_dir_all(CACHE_DIR).map_err(|e| format!("mkdir cache: {e}"))?; - fs::write(cache_index_path(cfg), txt).map_err(|e| format!("write index: {e}"))?; - Ok(()) -} -fn index_ensure(cfg: &Cfg) -> Result<(), String> { - let p = cache_index_path(cfg); - if p.exists() { - println!("Index already cached"); - return Ok(()); - } - index_refresh(cfg) -} - -fn resolve_and_fetch(cfg: &Cfg, name: &str) -> Result { - let idx = fs::read_to_string(cache_index_path(cfg)).map_err(|e| format!("read index: {e}"))?; - let mut best: Option<(String, String, String)> = None; // (filename, version, arch) - for r in parse_apkindex(&idx, name) { - if r.2 == cfg.arch || r.2 == "noarch" { - best = Some(match best { - None => r, - Some(b) => { - if r.1 > b.1 { - r - } else { - b - } - } - }); - } - } - let (fname, ver, _) = best.ok_or_else(|| { - format!( - "{name} not found in {}/{}/{}", - cfg.branch, cfg.repo, cfg.arch - ) - })?; - let url = format!("{}{}", repo_base(cfg), fname); - let dest = tmp_apk(name, &ver); - let dest_str = dest.to_str().unwrap(); - println!("Downloading {url} to {dest_str}..."); - sh_status(r#"curl -fsSL "$1" -o "$2""#, &[&url, dest_str])?; - Ok(dest) -} - -fn parse_apkindex(s: &str, want: &str) -> Vec<(String, String, String)> { - // returns (filename, version, arch) - let mut out = Vec::new(); - let mut p: Option<&str> = None; - let mut v: Option<&str> = None; - let mut a: Option<&str> = None; - let mut f: Option<&str> = None; - for line in s.lines().map(|l| l.trim_end()) { - if line.is_empty() { - if matches!(p, Some(n) if n==want) { - out.push(( - f.map(|x| x.to_string()) - .unwrap_or_else(|| format!("{}-{}.apk", want, v.unwrap_or_default())), - v.unwrap_or_default().to_string(), - a.unwrap_or_default().to_string(), - )); - } - p = None; - v = None; - a = None; - f = None; - continue; - } - if let Some(x) = line.strip_prefix("P:") { - p = Some(x.trim()); - continue; - } - if let Some(x) = line.strip_prefix("V:") { - v = Some(x.trim()); - continue; - } - if let Some(x) = line.strip_prefix("A:") { - a = Some(x.trim()); - continue; - } - if let Some(x) = line.strip_prefix("F:") { - f = Some(x.trim()); - continue; - } - } - if matches!(p, Some(n) if n==want) { - out.push(( - f.map(|x| x.to_string()) - .unwrap_or_else(|| format!("{}-{}.apk", want, v.unwrap_or_default())), - v.unwrap_or_default().to_string(), - a.unwrap_or_default().to_string(), - )); - } - out -} - -fn tmp_apk(name: &str, ver: &str) -> PathBuf { - let ts = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - Path::new("/tmp").join(format!( - "minapk-{}-{}-{}.apk", - name, - ver.replace('/', "_"), - ts - )) -} - -// ----- Install/remove/list ----- - fn add(root: &Path, apk: &Path, name_override: Option) -> Result<(), String> { if !apk.exists() { return Err(format!("{} missing", apk.display())); @@ -361,7 +177,7 @@ fn add(root: &Path, apk: &Path, name_override: Option) -> Result<(), Str let name = name_override .or_else(|| apk.file_stem().map(|s| s.to_string_lossy().to_string())) .ok_or("unable to derive package name; use --name")?; - let mut m = manifest_load(root)?; + let mut m = manifest_load()?; if m.contains_key(&name) { println!("installed {name}"); return Ok(()); @@ -379,13 +195,13 @@ fn add(root: &Path, apk: &Path, name_override: Option) -> Result<(), Str ], )?; m.insert(name.clone(), files); - manifest_save(root, &m)?; + manifest_save(&m)?; println!("installed {name}"); Ok(()) } fn remove(root: &Path, name: &str) -> Result<(), String> { - let mut m = manifest_load(root)?; + let mut m = manifest_load()?; let Some(entries) = m.remove(name) else { return Err(format!("package {name} is not installed")); }; @@ -416,13 +232,13 @@ fn remove(root: &Path, name: &str) -> Result<(), String> { for d in prune.into_iter().rev() { let _ = fs::remove_dir(&d); } - manifest_save(root, &m)?; + manifest_save(&m)?; println!("removed {name}"); Ok(()) } -fn list(root: &Path) -> Result<(), String> { - let m = manifest_load(root)?; +fn list() -> Result<(), String> { + let m = manifest_load()?; if m.is_empty() { println!("no packages installed"); } else { @@ -432,114 +248,3 @@ fn list(root: &Path) -> Result<(), String> { } Ok(()) } - -// ----- tar + shell ----- - -fn extract_filtered(root: &Path, apk: &Path, excludes: &[&str]) -> Result, String> { - let root = root - .to_str() - .ok_or_else(|| format!("non UTF-8 root: {}", root.display()))?; - let apk = apk - .to_str() - .ok_or_else(|| format!("non UTF-8 path: {}", apk.display()))?; - - // 1) list filtered - let mut list_args = vec![ - "-tf", - apk, - "--exclude=.PKGINFO", - "--exclude=.SIGN*", - ]; - for pat in excludes { - list_args.push("--exclude"); - list_args.push(pat); - } - let out = Command::new("tar") - .args(&list_args) - .output() - .map_err(|e| format!("tar -tf: {e}"))?; - if !out.status.success() { - return Err("tar list failed".into()); - } - - let files: Vec = String::from_utf8_lossy(&out.stdout) - .lines() - .filter_map(|l| { - let s = sanitize(l); - if s.is_empty() || s.ends_with('/') { - None - } else { - Some(s) - } - }) - .collect(); - - // 2) extract filtered - let mut ex_args = vec![ - "-xzC", - root, - "--exclude=.PKGINFO", - "--exclude=.SIGN*", - "-f", - apk, - ]; - for pat in excludes { - ex_args.push("--exclude"); - ex_args.push(pat); - } - let st = Command::new("tar") - .args(&ex_args) - .status() - .map_err(|e| format!("tar -xz: {e}"))?; - if !st.success() { - return Err("tar extraction failed".into()); - } - Ok(files) -} - -fn sanitize(e: &str) -> String { - let t = e.trim().trim_start_matches("./"); - if t.is_empty() { - return String::new(); - } - if t.starts_with('/') { - return t.trim_start_matches('/').to_string(); - } - if t.contains("../") { - return String::new(); - } - t.to_string() -} - -// shell helpers - -fn sh_out(script: &str, args: &[&str]) -> Result, String> { - let mut c = Command::new("sh"); - c.arg("-c").arg(script).arg("minapk"); - for a in args { - c.arg(a); - } - let out = c.output().map_err(|e| format!("spawn: {e}"))?; - if !out.status.success() { - let err = String::from_utf8_lossy(&out.stderr).trim().to_string(); - return Err(if err.is_empty() { - "command failed".into() - } else { - err - }); - } - Ok(out.stdout) -} -fn sh_status(script: &str, args: &[&str]) -> Result<(), String> { - let mut c = Command::new("sh"); - c.arg("-c").arg(script).arg("minapk"); - for a in args { - c.arg(a); - } - let st = c.status().map_err(|e| format!("spawn: {e}"))?; - if !st.success() { - return Err("command failed".into()); - } - Ok(()) -} - diff --git a/crates/minapk/src/manifest.rs b/crates/minapk/src/manifest.rs new file mode 100644 index 0000000..7699db7 --- /dev/null +++ b/crates/minapk/src/manifest.rs @@ -0,0 +1,57 @@ +use std::collections::BTreeMap; +use std::fs::{self, File}; +use std::io::{self, BufRead, BufReader, BufWriter, Write}; +use std::path::PathBuf; + +use crate::MINAPK_DATA; + +pub fn manifest_path() -> Result { + let p = PathBuf::from(MINAPK_DATA).join("manifset"); + if let Some(d) = p.parent() { + fs::create_dir_all(d).map_err(|e| format!("mkdir {}: {e}", d.display()))?; + } + Ok(p) +} +pub fn manifest_load() -> Result>, String> { + let p = manifest_path()?; + let f = match File::open(&p) { + Ok(f) => f, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(BTreeMap::new()), + Err(e) => return Err(format!("read {}: {e}", p.display())), + }; + let mut out: BTreeMap> = BTreeMap::new(); + let mut cur: Option = None; + let mut files: Vec = Vec::new(); + for line in BufReader::new(f).lines() { + let line = line.map_err(|e| format!("manifest read: {e}"))?; + if line.trim().is_empty() { + if let Some(n) = cur.take() { + out.insert(n, std::mem::take(&mut files)); + } + continue; + } + if cur.is_none() { + cur = Some(line.trim().into()); + } else { + files.push(line.trim().into()); + } + } + if let Some(n) = cur { + out.insert(n, files); + } + Ok(out) +} + +pub fn manifest_save(m: &BTreeMap>) -> Result<(), String> { + let p = manifest_path()?; + let mut w = + BufWriter::new(File::create(&p).map_err(|e| format!("write {}: {e}", p.display()))?); + for (n, files) in m { + writeln!(w, "{n}").map_err(|e| format!("manifest write: {e}"))?; + for f in files { + writeln!(w, "{f}").map_err(|e| format!("manifest write: {e}"))?; + } + writeln!(w).map_err(|e| format!("manifest write: {e}"))?; + } + w.flush().map_err(|e| format!("manifest flush: {e}")) +} diff --git a/crates/minapk/src/shell.rs b/crates/minapk/src/shell.rs new file mode 100644 index 0000000..2456b6f --- /dev/null +++ b/crates/minapk/src/shell.rs @@ -0,0 +1,115 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +pub fn space_left_on(location: &PathBuf) -> Result { + let a = sh_out("df $1 | awk 'NR==2 {print $3*1024}'", &[&location.to_string_lossy()])?; + Ok(String::from_utf8_lossy(&a) + .trim() + .parse() + .map_err(|_| "Could get available space")?) +} + +pub fn extract_filtered(root: &Path, apk: &Path, excludes: &[&str]) -> Result, String> { + let root = root + .to_str() + .ok_or_else(|| format!("non UTF-8 root: {}", root.display()))?; + let apk = apk + .to_str() + .ok_or_else(|| format!("non UTF-8 path: {}", apk.display()))?; + + // 1) list filtered + let mut list_args = vec!["-tf", apk, "--exclude=.PKGINFO", "--exclude=.SIGN*"]; + for pat in excludes { + list_args.push("--exclude"); + list_args.push(pat); + } + let out = Command::new("tar") + .args(&list_args) + .output() + .map_err(|e| format!("tar -tf: {e}"))?; + if !out.status.success() { + return Err("tar list failed".into()); + } + + let files: Vec = String::from_utf8_lossy(&out.stdout) + .lines() + .filter_map(|l| { + let s = sanitize(l); + if s.is_empty() || s.ends_with('/') { + None + } else { + Some(s) + } + }) + .collect(); + + // 2) extract filtered + let mut ex_args = vec![ + "-xzC", + root, + "--exclude=.PKGINFO", + "--exclude=.SIGN*", + "-f", + apk, + ]; + for pat in excludes { + ex_args.push("--exclude"); + ex_args.push(pat); + } + let st = Command::new("tar") + .args(&ex_args) + .status() + .map_err(|e| format!("tar -xz: {e}"))?; + if !st.success() { + return Err("tar extraction failed".into()); + } + Ok(files) +} + +#[must_use] +pub fn sanitize(e: &str) -> String { + let t = e.trim().trim_start_matches("./"); + if t.is_empty() { + return String::new(); + } + if t.starts_with('/') { + return t.trim_start_matches('/').to_string(); + } + if t.contains("../") { + return String::new(); + } + t.to_string() +} + +pub fn sh_out(script: &str, args: &[&str]) -> Result, String> { + let mut c = Command::new("sh"); + c.arg("-c").arg(script).arg("minapk"); + for a in args { + c.arg(a); + } + let out = c.output().map_err(|e| format!("spawn: {e}"))?; + if !out.status.success() { + let err = String::from_utf8_lossy(&out.stderr).trim().to_string(); + return Err(if err.is_empty() { + "command failed".into() + } else { + err + }); + } + Ok(out.stdout) +} + +pub fn sh_status(script: &str, args: &[&str]) -> Result<(), String> { + let mut c = Command::new("sh"); + c.arg("-c").arg(script).arg("minapk"); + for a in args { + c.arg(a); + } + let st = c.status().map_err(|e| format!("spawn: {e}"))?; + if !st.success() { + return Err("command failed".into()); + } + Ok(()) +} diff --git a/scripts/curl-tls.sh b/scripts/curl-tls.sh new file mode 100644 index 0000000..0518065 --- /dev/null +++ b/scripts/curl-tls.sh @@ -0,0 +1,5 @@ +#! /bin/sh +import.lua +mv cacert.pem /etc/ssl/certs +ln -s /etc/ssl/certs/cacert.pem /etc/ssl/cert.pem +echo tls-max = 1.2 > .curlrc diff --git a/scripts/setup-eth-dev.sh b/scripts/setup-eth-dev.sh new file mode 100644 index 0000000..2e5ed0f --- /dev/null +++ b/scripts/setup-eth-dev.sh @@ -0,0 +1,2 @@ +#! /bin/sh +ip link set eth1 up && ip addr add 192.168.2.1/30 dev eth1 && ip route add default via 192.168.2.2 && echo nameserver 1.1.1.1 >/etc/resolv.conf