[WIP] Minapk rework

This commit is contained in:
2025-11-01 23:28:52 +01:00
parent af79be12b1
commit 094eeafa07
9 changed files with 411 additions and 369 deletions

View File

@@ -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

View File

@@ -3,3 +3,5 @@ name = "minapk"
version = "0.1.0"
edition = "2024"
[lints]
workspace = true

124
crates/minapk/src/index.rs Normal file
View File

@@ -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<PathBuf, String> {
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
))
}

31
crates/minapk/src/lib.rs Normal file
View File

@@ -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<String>,
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;

View File

@@ -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<String>,
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 <name>")?;
remove(&root, name)
}
"list" => list(&root),
"list" => list(),
"update" => {
let what = pos.first().ok_or("usage: update <index|name>")?;
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>), String> {
let mut cfg = Cfg::default();
@@ -142,11 +155,13 @@ fn parse_args(argv: &[String]) -> Result<(Cfg, String, Vec<String>), 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 <index|name>".into()
}
// ----- Manifest -----
fn manifest_path(root: &Path) -> Result<PathBuf, String> {
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<BTreeMap<String, Vec<String>>, 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<String, Vec<String>> = BTreeMap::new();
let mut cur: Option<String> = None;
let mut files: Vec<String> = 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<String, Vec<String>>) -> 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<PathBuf, String> {
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<String>) -> Result<(), String> {
if !apk.exists() {
return Err(format!("{} missing", apk.display()));
@@ -361,7 +177,7 @@ fn add(root: &Path, apk: &Path, name_override: Option<String>) -> 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<String>) -> 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<Vec<String>, 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> = 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<Vec<u8>, 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(())
}

View File

@@ -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<PathBuf, String> {
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<BTreeMap<String, Vec<String>>, 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<String, Vec<String>> = BTreeMap::new();
let mut cur: Option<String> = None;
let mut files: Vec<String> = 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<String, Vec<String>>) -> 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}"))
}

115
crates/minapk/src/shell.rs Normal file
View File

@@ -0,0 +1,115 @@
use std::{
path::{Path, PathBuf},
process::Command,
};
pub fn space_left_on(location: &PathBuf) -> Result<usize, String> {
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<Vec<String>, 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> = 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<Vec<u8>, 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(())
}

5
scripts/curl-tls.sh Normal file
View File

@@ -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

2
scripts/setup-eth-dev.sh Normal file
View File

@@ -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