use anyhow::{bail, Context, Error, Ok, Result}; use std::{ fs::{create_dir_all, remove_file, write, File, OpenOptions}, io::{ErrorKind::AlreadyExists, ErrorKind::NotFound, Write}, path::Path, }; use crate::defs; use std::fs::metadata; #[allow(unused_imports)] use std::fs::{set_permissions, Permissions}; #[cfg(unix)] use std::os::unix::prelude::PermissionsExt; use hole_punch::*; use std::io::{Read, Seek, SeekFrom}; #[cfg(any(target_os = "linux", target_os = "android"))] use rustix::{ process, thread::{move_into_link_name_space, unshare, LinkNameSpaceType, UnshareFlags}, }; pub fn ensure_clean_dir(dir: &str) -> Result<()> { let path = Path::new(dir); log::debug!("ensure_clean_dir: {}", path.display()); if path.exists() { log::debug!("ensure_clean_dir: {} exists, remove it", path.display()); std::fs::remove_dir_all(path)?; } Ok(std::fs::create_dir_all(path)?) } pub fn ensure_file_exists>(file: T) -> Result<()> { match File::options().write(true).create_new(true).open(&file) { std::result::Result::Ok(_) => Ok(()), Err(err) => { if err.kind() == AlreadyExists && file.as_ref().is_file() { Ok(()) } else { Err(Error::from(err)) .with_context(|| format!("{} is not a regular file", file.as_ref().display())) } } } } pub fn ensure_dir_exists>(dir: T) -> Result<()> { let result = create_dir_all(&dir).map_err(Error::from); if dir.as_ref().is_dir() { result } else if result.is_ok() { bail!("{} is not a regular directory", dir.as_ref().display()) } else { result } } pub fn ensure_binary>( path: T, contents: &[u8], ignore_if_exist: bool, ) -> Result<()> { if ignore_if_exist && path.as_ref().exists() { return Ok(()); } ensure_dir_exists(path.as_ref().parent().ok_or_else(|| { anyhow::anyhow!( "{} does not have parent directory", path.as_ref().to_string_lossy() ) })?)?; if let Err(e) = remove_file(path.as_ref()) { if e.kind() != NotFound { return Err(Error::from(e)) .with_context(|| format!("failed to unlink {}", path.as_ref().display())); } } write(&path, contents)?; #[cfg(unix)] set_permissions(&path, Permissions::from_mode(0o755))?; Ok(()) } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn getprop(prop: &str) -> Option { android_properties::getprop(prop).value() } #[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn getprop(_prop: &str) -> Option { unimplemented!() } pub fn is_safe_mode() -> bool { let safemode = getprop("persist.sys.safemode") .filter(|prop| prop == "1") .is_some() || getprop("ro.sys.safemode") .filter(|prop| prop == "1") .is_some(); log::info!("safemode: {}", safemode); if safemode { return true; } let safemode = crate::ksu::check_kernel_safemode(); log::info!("kernel_safemode: {}", safemode); safemode } pub fn get_zip_uncompressed_size(zip_path: &str) -> Result { let mut zip = zip::ZipArchive::new(std::fs::File::open(zip_path)?)?; let total: u64 = (0..zip.len()) .map(|i| zip.by_index(i).unwrap().size()) .sum(); Ok(total) } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn switch_mnt_ns(pid: i32) -> Result<()> { use rustix::{ fd::AsFd, fs::{open, Mode, OFlags}, }; let path = format!("/proc/{pid}/ns/mnt"); let fd = open(path, OFlags::RDONLY, Mode::from_raw_mode(0))?; let current_dir = std::env::current_dir(); move_into_link_name_space(fd.as_fd(), Some(LinkNameSpaceType::Mount))?; if let std::result::Result::Ok(current_dir) = current_dir { let _ = std::env::set_current_dir(current_dir); } Ok(()) } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn unshare_mnt_ns() -> Result<()> { unshare(UnshareFlags::NEWNS)?; Ok(()) } fn switch_cgroup(grp: &str, pid: u32) { let path = Path::new(grp).join("cgroup.procs"); if !path.exists() { return; } let fp = OpenOptions::new().append(true).open(path); if let std::result::Result::Ok(mut fp) = fp { let _ = writeln!(fp, "{pid}"); } } pub fn switch_cgroups() { let pid = std::process::id(); switch_cgroup("/acct", pid); switch_cgroup("/dev/cg2_bpf", pid); switch_cgroup("/sys/fs/cgroup", pid); if getprop("ro.config.per_app_memcg") .filter(|prop| prop == "false") .is_none() { switch_cgroup("/dev/memcg/apps", pid); } } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn umask(mask: u32) { process::umask(rustix::fs::Mode::from_raw_mode(mask)); } #[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn umask(_mask: u32) { unimplemented!("umask is not supported on this platform") } pub fn has_magisk() -> bool { which::which("magisk").is_ok() } pub fn get_tmp_path() -> &'static str { if metadata(defs::TEMP_DIR_LEGACY).is_ok() { return defs::TEMP_DIR_LEGACY; } if metadata(defs::TEMP_DIR).is_ok() { return defs::TEMP_DIR; } "" } // TODO: use libxcp to improve the speed if cross's MSRV is 1.70 pub fn copy_sparse_file, Q: AsRef>(src: P, dst: Q) -> Result<()> { let mut src_file = File::open(src.as_ref())?; let mut dst_file = OpenOptions::new() .write(true) .create(true) .truncate(true) .open(dst.as_ref())?; dst_file.set_len(src_file.metadata()?.len())?; let segments = src_file.scan_chunks()?; for segment in segments { if let SegmentType::Data = segment.segment_type { let start = segment.start; let end = segment.end; src_file.seek(SeekFrom::Start(start))?; dst_file.seek(SeekFrom::Start(start))?; let mut buffer = [0; 4096]; let mut total_bytes_copied = 0; while total_bytes_copied < end - start { let bytes_to_read = std::cmp::min(buffer.len() as u64, end - start - total_bytes_copied); let bytes_read = src_file.read(&mut buffer[..bytes_to_read as usize])?; if bytes_read == 0 { break; } dst_file.write_all(&buffer[..bytes_read])?; total_bytes_copied += bytes_read as u64; } } } Ok(()) } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn punch_hole(src: impl AsRef) -> Result<()> { let mut src_file = OpenOptions::new().write(true).read(true).open(src)?; let st = rustix::fs::fstat(&src_file)?; let bufsz = st.st_blksize; let mut buf = vec![0u8; bufsz as usize]; let mut ct = 0; let mut hole_sz = 0; let mut hole_start = 0; let segments = src_file.scan_chunks()?; for segment in segments { if segment.segment_type != SegmentType::Data { continue; } let mut off = segment.start; let end = segment.end + 1; while off < end { let mut rsz = rustix::io::pread(&src_file, &mut buf, off)? as u64; if rsz > 0 && rsz > end - off { // exceed the end of the boundary rsz = end - off; } if rsz == 0 { break; } if buf.iter().all(|&x| x == 0) { // the whole buf is zero, mark it as a hole if hole_sz == 0 { hole_start = off; } // for continuous zero, we can merge them into a bigger hole hole_sz += rsz; } else if hole_sz > 0 { if let Err(e) = rustix::fs::fallocate( &src_file, rustix::fs::FallocateFlags::PUNCH_HOLE | rustix::fs::FallocateFlags::KEEP_SIZE, hole_start, hole_sz, ) { log::warn!("Failed to punch hole: {:?}", e); } ct += hole_sz; hole_sz = 0; hole_start = 0; } off += rsz; } // if the last segment is a hole, we need to punch it if hole_sz > 0 { let mut alloc_sz = hole_sz; if off >= end { alloc_sz += st.st_blksize as u64; } if let Err(e) = rustix::fs::fallocate( &src_file, rustix::fs::FallocateFlags::PUNCH_HOLE | rustix::fs::FallocateFlags::KEEP_SIZE, hole_start, alloc_sz, ) { log::warn!("Failed to punch hole: {:?}", e); } ct += hole_sz; } } log::info!( "Punched {} of hole", humansize::format_size(ct, humansize::DECIMAL) ); Ok(()) } #[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn punch_hole(src: impl AsRef) -> Result<()> { unimplemented!() }