mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-29 04:02:47 +01:00
Handle restricted overlay directories in storage settings and migration
This commit is contained in:
1 parent
6cb052f732
commit
c647aa28a0
3 files changed
+180
-10
No files matched your search
+44
-4
@@ -1002,15 +1002,21 @@ pub fn storage_status(home: &Path) -> Result<Value> {
|
||||
let contexts = config.contexts.unwrap_or_else(|| home.join(".local/share/lepton/contexts"));
|
||||
let db = load(home)?;
|
||||
let mut context_bytes = 0u64;
|
||||
let mut contexts_incomplete = false;
|
||||
for id in &db.owned_contexts {
|
||||
if safe(id) { context_bytes = context_bytes.saturating_add(crate::storage_location::bytes(&contexts.join(id))?); }
|
||||
if safe(id) {
|
||||
let (size, incomplete) = crate::storage_location::display_bytes(&contexts.join(id))?;
|
||||
context_bytes = context_bytes.saturating_add(size);
|
||||
contexts_incomplete |= incomplete;
|
||||
}
|
||||
}
|
||||
let (apk_bytes, apk_incomplete) = crate::storage_location::display_bytes(&apk)?;
|
||||
let journal = fs::read(storage_journal_path(home)).ok().and_then(|b|serde_json::from_slice::<Value>(&b).ok());
|
||||
let committed = journal.as_ref().is_some_and(|v| v["target"] == json!(if v["kind"] == "apk" { &apk } else { &contexts }));
|
||||
if committed {
|
||||
let _ = fs::remove_file(storage_journal_path(home));
|
||||
}
|
||||
Ok(json!({"apk":{"path":apk,"bytes":crate::storage_location::bytes(&apk)?},"contexts":{"path":contexts,"bytes":context_bytes},"interrupted":if committed {None} else {journal}}))
|
||||
Ok(json!({"apk":{"path":apk,"bytes":apk_bytes,"sizeIncomplete":apk_incomplete},"contexts":{"path":contexts,"bytes":context_bytes,"sizeIncomplete":contexts_incomplete},"interrupted":if committed {None} else {journal}}))
|
||||
}
|
||||
pub fn migrate_storage(home: &Path, kind: &str, target: &Path, cancel: Cancellation, progress: Arc<dyn Fn(Value) + Send + Sync>) -> Result<Value> {
|
||||
ensure!(matches!(kind, "apk" | "contexts"), "无效存储类型");
|
||||
@@ -1075,11 +1081,11 @@ pub fn migrate_storage(home: &Path, kind: &str, target: &Path, cancel: Cancellat
|
||||
progress(json!({"step":"cleaning"}));
|
||||
let mut warnings = Vec::new();
|
||||
if kind == "apk" {
|
||||
if let Err(error) = fs::remove_dir_all(&source) { warnings.push(error.to_string()); }
|
||||
if let Err(error) = crate::storage_location::remove_tree(&source) { warnings.push(error.to_string()); }
|
||||
} else {
|
||||
for id in &db.owned_contexts {
|
||||
if safe(id) && source.join(id).exists() {
|
||||
if let Err(error) = fs::remove_dir_all(source.join(id)) { warnings.push(error.to_string()); }
|
||||
if let Err(error) = crate::storage_location::remove_tree(&source.join(id)) { warnings.push(error.to_string()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1101,10 +1107,18 @@ mod storage_migration_tests {
|
||||
fs::create_dir(&home).unwrap();
|
||||
let original_apk = init(&home).unwrap();
|
||||
fs::write(original_apk.join("apks/example.apk"), b"apk").unwrap();
|
||||
let apk_work = original_apk.join("recovery/old/baked/app_workdir/work");
|
||||
fs::create_dir_all(&apk_work).unwrap();
|
||||
fs::write(apk_work.join("overlay-state"), b"private").unwrap();
|
||||
fs::set_permissions(&apk_work, fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let original_contexts = contexts_root(&home);
|
||||
let baked = original_contexts.join("managed/baked/data_overlay/system");
|
||||
fs::create_dir_all(&baked).unwrap();
|
||||
fs::write(baked.join("packages.xml"), b"<packages/>").unwrap();
|
||||
let context_work = original_contexts.join("managed/baked/data_workdir/work");
|
||||
fs::create_dir_all(&context_work).unwrap();
|
||||
fs::write(context_work.join("overlay-state"), b"private").unwrap();
|
||||
fs::set_permissions(&context_work, fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let external = original_contexts.join("external/baked/data_overlay/system");
|
||||
fs::create_dir_all(&external).unwrap();
|
||||
fs::write(external.join("packages.xml"), b"<packages/>").unwrap();
|
||||
@@ -1115,6 +1129,8 @@ mod storage_migration_tests {
|
||||
migrate_storage(&home,"apk",&apk_target,Cancellation::default(),Arc::new(|_|{})).unwrap();
|
||||
assert_eq!(root(&home), apk_target);
|
||||
assert_eq!(fs::read(root(&home).join("apks/example.apk")).unwrap(), b"apk");
|
||||
assert!(!original_apk.exists());
|
||||
assert_eq!(fs::metadata(apk_target.join("recovery/old/baked/app_workdir/work")).unwrap().permissions().mode() & 0o777, 0);
|
||||
assert!(original_contexts.join("managed/baked").is_dir());
|
||||
let context_target = temp.path().join("android-new");
|
||||
migrate_storage(&home,"contexts",&context_target,Cancellation::default(),Arc::new(|_|{})).unwrap();
|
||||
@@ -1126,6 +1142,8 @@ mod storage_migration_tests {
|
||||
let found = containers(&home,&load(&home).unwrap());
|
||||
assert!(found.iter().any(|c|c.id=="managed" && c.baked.starts_with(&context_target)));
|
||||
assert!(found.iter().any(|c|c.id=="external" && c.baked.starts_with(&original_contexts)));
|
||||
fs::set_permissions(apk_target.join("recovery/old/baked/app_workdir/work"), fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::set_permissions(context_target.join("managed/baked/data_workdir/work"), fs::Permissions::from_mode(0o700)).unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn interrupted_apk_copy_resumes_without_touching_original() {
|
||||
@@ -1145,6 +1163,28 @@ mod storage_migration_tests {
|
||||
assert_eq!(fs::read(target.join("apks/keep.apk")).unwrap(),b"saved apk");
|
||||
assert!(!storage_journal_path(&home).exists());
|
||||
}
|
||||
#[test]
|
||||
fn storage_status_survives_unreadable_overlay_workdirs() {
|
||||
if unsafe { libc::geteuid() } == 0 { return; }
|
||||
let _serial = TEST_SERIAL.lock().unwrap();
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let home = temp.path().join("home");
|
||||
fs::create_dir(&home).unwrap();
|
||||
let apk_root = init(&home).unwrap();
|
||||
let apk_work = apk_root.join("recovery/old/baked/app_workdir/work");
|
||||
fs::create_dir_all(&apk_work).unwrap();
|
||||
let context_work = contexts_root(&home).join("managed/baked/data_workdir/work");
|
||||
fs::create_dir_all(&context_work).unwrap();
|
||||
let mut db = load(&home).unwrap();
|
||||
db.owned_contexts.push("managed".into());
|
||||
save(&home, &db).unwrap();
|
||||
for path in [&apk_work, &context_work] { fs::set_permissions(path, fs::Permissions::from_mode(0o000)).unwrap(); }
|
||||
let status = storage_status(&home);
|
||||
for path in [&apk_work, &context_work] { fs::set_permissions(path, fs::Permissions::from_mode(0o700)).unwrap(); }
|
||||
let status = status.unwrap();
|
||||
assert_eq!(status["apk"]["sizeIncomplete"], true);
|
||||
assert_eq!(status["contexts"]["sizeIncomplete"], true);
|
||||
}
|
||||
}
|
||||
pub fn launcher(home: &Path) -> Vec<Value> {
|
||||
let Ok(db) = load(home) else { return vec![] };
|
||||
|
||||
+134
-4
@@ -4,8 +4,12 @@ use sha2::{Digest, Sha256};
|
||||
use std::{collections::BTreeMap, ffi::CString, fs, io::Read, os::unix::fs::{MetadataExt, FileTypeExt, symlink, PermissionsExt}, path::{Path, PathBuf}, process::Command};
|
||||
|
||||
pub fn bytes(path: &Path) -> Result<u64> {
|
||||
if !path.exists() { return Ok(0); }
|
||||
let root = if fs::symlink_metadata(path)?.file_type().is_symlink() { path.canonicalize()? } else { path.to_path_buf() };
|
||||
let meta = match fs::symlink_metadata(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let root = if meta.file_type().is_symlink() { path.canonicalize()? } else { path.to_path_buf() };
|
||||
let mut total = 0u64;
|
||||
fn walk(path: &Path, total: &mut u64) -> Result<()> {
|
||||
let m = fs::symlink_metadata(path)?;
|
||||
@@ -19,6 +23,44 @@ pub fn bytes(path: &Path) -> Result<u64> {
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
/// The settings page can show a lower bound when a container contains
|
||||
/// root-owned overlay work directories. Migration still uses strict `bytes`.
|
||||
pub fn display_bytes(path: &Path) -> Result<(u64, bool)> {
|
||||
let meta = match fs::symlink_metadata(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok((0, false)),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => return Ok((0, true)),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let root = if meta.file_type().is_symlink() {
|
||||
match path.canonicalize() {
|
||||
Ok(root) => root,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => return Ok((0, true)),
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
} else { path.to_path_buf() };
|
||||
fn walk(path: &Path, total: &mut u64, incomplete: &mut bool) -> Result<()> {
|
||||
let meta = match fs::symlink_metadata(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { *incomplete = true; return Ok(()); }
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
if meta.is_file() { *total = total.saturating_add(meta.len()); }
|
||||
if meta.is_dir() && !meta.file_type().is_symlink() {
|
||||
let entries = match fs::read_dir(path) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { *incomplete = true; return Ok(()); }
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
for entry in entries { walk(&entry?.path(), total, incomplete)?; }
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let (mut total, mut incomplete) = (0, false);
|
||||
walk(&root, &mut total, &mut incomplete)?;
|
||||
Ok((total, incomplete))
|
||||
}
|
||||
|
||||
fn inventory(path: &Path) -> Result<BTreeMap<PathBuf, String>> {
|
||||
let mut files = BTreeMap::new();
|
||||
fn walk(root: &Path, rel: &Path, files: &mut BTreeMap<PathBuf, String>) -> Result<()> {
|
||||
@@ -46,7 +88,10 @@ fn inventory(path: &Path) -> Result<BTreeMap<PathBuf, String>> {
|
||||
Ok(files)
|
||||
}
|
||||
pub fn verified_equal(source: &Path, target: &Path) -> Result<bool> {
|
||||
Ok(source.is_dir() && target.is_dir() && inventory(source)? == inventory(target)?)
|
||||
if !source.is_dir() || !target.is_dir() { return Ok(false); }
|
||||
let _source_readable = ReadableDirs::prepare(source)?;
|
||||
let _target_readable = ReadableDirs::prepare(target)?;
|
||||
Ok(inventory(source)? == inventory(target)?)
|
||||
}
|
||||
fn selected_source(source: &Path, name: &str, follow_root_links: bool) -> Result<PathBuf> {
|
||||
let path = source.join(name);
|
||||
@@ -120,8 +165,55 @@ fn prepare_space(parent: &Path, needed: u64) -> Result<()> {
|
||||
result
|
||||
}
|
||||
|
||||
struct ReadableDirs {
|
||||
root: PathBuf,
|
||||
changed: Vec<(PathBuf, u32)>,
|
||||
}
|
||||
impl ReadableDirs {
|
||||
fn prepare(root: &Path) -> Result<Self> {
|
||||
Self::prepare_with(root, 0o500)
|
||||
}
|
||||
fn prepare_with(root: &Path, required: u32) -> Result<Self> {
|
||||
let mut guard = Self { root: root.to_path_buf(), changed: Vec::new() };
|
||||
if root.exists() { guard.walk(root, required)?; }
|
||||
Ok(guard)
|
||||
}
|
||||
fn walk(&mut self, path: &Path, required: u32) -> Result<()> {
|
||||
let meta = fs::symlink_metadata(path)?;
|
||||
if !meta.is_dir() || meta.file_type().is_symlink() { return Ok(()); }
|
||||
let mode = meta.permissions().mode() & 0o7777;
|
||||
if mode & required != required {
|
||||
ensure!(meta.uid() == unsafe { libc::geteuid() } || unsafe { libc::geteuid() } == 0,
|
||||
"存储目录权限不足,无法迁移:{}", path.display());
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(mode | required))?;
|
||||
self.changed.push((path.strip_prefix(&self.root)?.to_path_buf(), mode));
|
||||
}
|
||||
for entry in fs::read_dir(path)? { self.walk(&entry?.path(), required)?; }
|
||||
Ok(())
|
||||
}
|
||||
fn restore_copy(&self, copy: &Path) -> Result<()> {
|
||||
for (relative, mode) in self.changed.iter().rev() {
|
||||
fs::set_permissions(copy.join(relative), fs::Permissions::from_mode(*mode))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Drop for ReadableDirs {
|
||||
fn drop(&mut self) {
|
||||
for (relative, mode) in self.changed.iter().rev() {
|
||||
let _ = fs::set_permissions(self.root.join(relative), fs::Permissions::from_mode(*mode));
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn remove_tree(path: &Path) -> Result<()> {
|
||||
let _readable = ReadableDirs::prepare_with(path, 0o700)?;
|
||||
fs::remove_dir_all(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn copy_verified(source: &Path, target: &Path, mut progress: impl FnMut(&str)) -> Result<()> {
|
||||
let target = validate_target(source, target, false)?;
|
||||
let readable = ReadableDirs::prepare(source)?;
|
||||
let parent = target.parent().unwrap();
|
||||
prepare_space(parent, bytes(source)?)?;
|
||||
let stage = parent.join(format!(".framely-storage-{:016x}", rand::random::<u64>()));
|
||||
@@ -137,6 +229,7 @@ pub fn copy_verified(source: &Path, target: &Path, mut progress: impl FnMut(&str
|
||||
let source_files = if source.exists() { inventory(source)? } else { BTreeMap::new() };
|
||||
let target_files = inventory(&stage)?;
|
||||
ensure!(source_files == target_files || (!source.exists() && target_files.len() == 1), "迁移后的文件校验失败,仍使用原位置");
|
||||
readable.restore_copy(&stage)?;
|
||||
progress("switching");
|
||||
if target.exists() { fs::remove_dir(&target)?; }
|
||||
fs::rename(&stage, &target)?;
|
||||
@@ -151,7 +244,12 @@ pub fn copy_selected(source: &Path, target: &Path, names: &[String], follow_root
|
||||
let target = validate_target(source, target, false)?;
|
||||
let parent = target.parent().unwrap();
|
||||
let mut needed = 0u64;
|
||||
for name in names { needed = needed.saturating_add(bytes(&selected_source(source, name, follow_root_links)?)?); }
|
||||
let mut readable = Vec::new();
|
||||
for name in names {
|
||||
let from = selected_source(source, name, follow_root_links)?;
|
||||
readable.push((name.clone(), ReadableDirs::prepare(&from)?));
|
||||
needed = needed.saturating_add(bytes(&from)?);
|
||||
}
|
||||
prepare_space(parent, needed)?;
|
||||
let stage = parent.join(format!(".framely-storage-{:016x}", rand::random::<u64>()));
|
||||
fs::create_dir(&stage)?;
|
||||
@@ -170,6 +268,7 @@ pub fn copy_selected(source: &Path, target: &Path, names: &[String], follow_root
|
||||
let from = selected_source(source, name, follow_root_links)?;
|
||||
if from.exists() { ensure!(inventory(&from)? == inventory(&stage.join(name))?, "容器迁移校验失败:{name}"); }
|
||||
}
|
||||
for (name, guard) in &readable { if stage.join(name).exists() { guard.restore_copy(&stage.join(name))?; } }
|
||||
progress("switching");
|
||||
if target.exists() { fs::remove_dir(&target)?; }
|
||||
fs::rename(&stage, &target)?;
|
||||
@@ -211,9 +310,16 @@ mod tests {
|
||||
fs::create_dir(source.join(name)).unwrap();
|
||||
fs::write(source.join(name).join("state"), name).unwrap();
|
||||
}
|
||||
let work = source.join("managed/app_workdir/work");
|
||||
fs::create_dir_all(&work).unwrap();
|
||||
fs::write(work.join("overlay-state"), b"private").unwrap();
|
||||
fs::set_permissions(&work, fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let target = temp.path().join("android");
|
||||
copy_selected(&source, &target, &["managed".into()], false, |_| {}).unwrap();
|
||||
assert_eq!(fs::read_to_string(target.join("managed/state")).unwrap(), "managed");
|
||||
assert!(verified_selected(&source, &target, &["managed".into()], false).unwrap());
|
||||
fs::set_permissions(&work, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::set_permissions(target.join("managed/app_workdir/work"), fs::Permissions::from_mode(0o700)).unwrap();
|
||||
assert!(!target.join("external").exists());
|
||||
assert!(source.join("external/state").is_file());
|
||||
}
|
||||
@@ -234,4 +340,28 @@ mod tests {
|
||||
assert_eq!(fs::read(target.join("plugins/manifest.json")).unwrap(),b"plugin");
|
||||
assert!(verified_selected(&source,&target,&["plugins".into()],true).unwrap());
|
||||
}
|
||||
#[test]
|
||||
fn display_size_marks_unreadable_overlay_work_without_hiding_migration_error() {
|
||||
if unsafe { libc::geteuid() } == 0 { return; }
|
||||
let temp = tempfile::tempdir_in(std::env::current_dir().unwrap()).unwrap();
|
||||
fs::write(temp.path().join("visible"), b"saved").unwrap();
|
||||
let work = temp.path().join("app_workdir/work");
|
||||
fs::create_dir_all(&work).unwrap();
|
||||
fs::write(work.join("overlay-state"), b"private").unwrap();
|
||||
fs::set_permissions(&work, fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let shown = display_bytes(temp.path());
|
||||
let strict = bytes(temp.path());
|
||||
assert_eq!(shown.unwrap(), (5, true));
|
||||
assert!(strict.is_err());
|
||||
let target = temp.path().parent().unwrap().join(format!("framely-storage-test-{:016x}", rand::random::<u64>()));
|
||||
copy_verified(temp.path(), &target, |_| {}).unwrap();
|
||||
assert_eq!(fs::metadata(&work).unwrap().permissions().mode() & 0o777, 0);
|
||||
let copied_work = target.join("app_workdir/work");
|
||||
assert_eq!(fs::metadata(&copied_work).unwrap().permissions().mode() & 0o777, 0);
|
||||
assert!(verified_equal(temp.path(), &target).unwrap());
|
||||
fs::set_permissions(&work, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
fs::set_permissions(&copied_work, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
assert_eq!(fs::read(copied_work.join("overlay-state")).unwrap(), b"private");
|
||||
fs::remove_dir_all(target).unwrap();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user