diff options
author | Matthew Schauer <matthew.schauer@e10x.net> | 2020-06-18 19:25:29 -0700 |
---|---|---|
committer | Matthew Schauer <matthew.schauer@e10x.net> | 2020-06-18 19:25:29 -0700 |
commit | d7566b5b7c7fcd6467f95f10203dc4b30d2165e1 (patch) | |
tree | 70b56f2cf7b42471dbf6179f9730a4850c4b31e7 | |
parent | 13b29a539bb63c0bca07fd4e8d5c0da4569277b8 (diff) |
Rough working API
-rw-r--r-- | src/lib.rs | 463 |
1 files changed, 439 insertions, 24 deletions
@@ -11,9 +11,12 @@ mod bindings { include!(concat!(env!("OUT_DIR"), "/bindings.rs")); } -use std::collections::HashMap; -use std::ffi::{CStr, CString}; +use std::collections::{HashMap, HashSet}; +use std::ffi::{CStr, CString, OsString}; +use std::io; +use std::io::{Read, Seek}; use std::mem::MaybeUninit; +use std::path::{Path, PathBuf, Component}; use std::ptr; use bindings::*; use num_derive::FromPrimitive; @@ -44,10 +47,15 @@ pub enum LibError { #[derive(Error, Debug)] pub enum SquashfsError { - #[error("Input contains an invalid null character")] NullInput(std::ffi::NulError), + #[error("Input contains an invalid null character")] NullInput(#[from] std::ffi::NulError), + #[error("Encoded string is not valid UTF-8")] Utf8(#[from] std::string::FromUtf8Error), + #[error("OS string is not valid UTF-8")] OsUtf8(OsString), #[error("{0}: {1}")] LibraryError(String, LibError), #[error("{0}: Unknown error {1} in Squashfs library")] UnknownLibraryError(String, i32), #[error("{0}: Squashfs library did not return expected value")] LibraryReturnError(String), + #[error("{0}")] LibraryNullError(String), + #[error("Symbolic link loop detected containing {0}")] LinkLoop(PathBuf), + #[error("{0} is type {1}, not {2}")] WrongType(PathBuf, String, String), } type Result<T> = std::result::Result<T, SquashfsError>; @@ -62,37 +70,423 @@ fn sfs_check(code: i32, desc: &str) -> Result<i32> { } } -fn sfs_err(desc: &str) -> Result<()> { - Err(SquashfsError::LibraryReturnError(desc.to_string())) +fn sfs_init<T>(init: &dyn Fn(*mut T) -> i32, err: &str) -> Result<T> { + let mut ret: MaybeUninit<T> = MaybeUninit::uninit(); + sfs_check(init(ret.as_mut_ptr()), err)?; + Ok(unsafe { ret.assume_init() }) +} + +fn sfs_init_ptr<T>(init: &dyn Fn(*mut *mut T) -> i32, err: &str) -> Result<*mut T> { + let mut ret: *mut T = ptr::null_mut(); + sfs_check(init(&mut ret), err)?; + if ret.is_null() { Err(SquashfsError::LibraryReturnError(err.to_string())) } + else { Ok(ret) } +} + +fn sfs_init_check_null<T>(init: &dyn Fn() -> *mut T, err: &str) -> Result<*mut T> { + let ret = init(); + if ret.is_null() { Err(SquashfsError::LibraryNullError(err.to_string())) } + else { Ok(ret) } +} + +// Canonicalize without requiring the path to actually exist in the filesystem +fn dumb_canonicalize(path: &Path) -> PathBuf { + let mut ret = PathBuf::new(); + for part in path.components() { + match part { + Component::Prefix(_) => panic!("What is this, Windows?"), + Component::CurDir => (), + Component::RootDir => ret.clear(), + Component::ParentDir => { ret.pop(); }, + Component::Normal(p) => ret.push(p), + } + } + ret +} + +fn path_to_string(path: &Path) -> Result<String> { + Ok(path.to_str().ok_or_else(|| SquashfsError::OsUtf8(path.as_os_str().to_os_string()))?.to_string()) } const NO_XATTRS: u32 = 0xffffffff; +#[derive(Debug)] +pub struct Dir<'a> { + node: &'a Node<'a>, + reader: *mut sqfs_dir_reader_t, +} + +impl<'a> Dir<'a> { + fn new(node: &'a Node) -> Result<Self> { + let reader = sfs_init_check_null(&|| unsafe { + sqfs_dir_reader_create(&*node.container.superblock, node.container.compressor, node.container.file, 0) + }, "Couldn't create directory reader")?; + unsafe { + sfs_check(sqfs_dir_reader_open_dir(reader, node.inode, 0), "Couldn't open directory")?; + } + Ok(Self { node: node, reader: reader }) + } + + pub fn reset(&mut self) { + unsafe { sqfs_dir_reader_rewind(self.reader); } + } + + fn read(&self) -> Result<Node> { + let entry = sfs_init_ptr(&|x| unsafe { + sqfs_dir_reader_read(self.reader, x) + }, "Couldn't read directory entries")?; + let name_bytes = unsafe { (*entry).name.as_slice((*entry).size as usize + 1) }; + let name = String::from_utf8(name_bytes.to_vec())?; + let node = sfs_init_ptr(&|x| unsafe { + sqfs_dir_reader_get_inode(self.reader, x) + }, "Couldn't read directory entry inode")?; + Node::new(self.node.container, node, self.node.path.join(name)) + } + + pub fn child(&self, name: &str) -> Result<Node> { + unsafe { sfs_check(sqfs_dir_reader_find(self.reader, CString::new(name)?.as_ptr()), &format!("Couldn't find child \"{}\"", name))? }; + self.read() + } + + pub fn node(&self) -> &'a Node { + self.node + } +} + +impl<'a> std::iter::Iterator for Dir<'a> { + type Item = Node<'a>; + + fn next(&mut self) -> Option<Self::Item> { + // TODO Figure out lifetime issue preventing this from just being self.read().ok() + match self.read() { + Err(_) => None, + Ok(node) => Node::new(self.node.container, node.inode, node.path.clone()).ok(), + } + } +} + +#[derive(Debug)] +pub struct File<'a> { + node: &'a Node<'a>, + reader: *mut sqfs_data_reader_t, + offset: u64, +} + +impl<'a> File<'a> { + fn new(node: &'a Node) -> Result<Self> { + let reader = sfs_init_check_null(&|| unsafe { + sqfs_data_reader_create(node.container.file, node.container.superblock.block_size as u64, node.container.compressor, 0) + }, "Couldn't create data reader")?; + unsafe { sfs_check(sqfs_data_reader_load_fragment_table(reader, &*node.container.superblock), "Couldn't load fragment table")? }; + Ok(Self { node: node, reader: reader, offset: 0 }) + } + + pub fn size(&self) -> u64 { + let mut ret: u64 = 0; + unsafe { sqfs_inode_get_file_size(self.node.inode, &mut ret) }; + ret + } + + pub fn node(&self) -> &'a Node { + self.node + } +} + +impl<'a> Read for File<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + if self.offset >= self.size() { Ok(0) } + else { + let res = unsafe { sfs_check(sqfs_data_reader_read(self.reader, self.node.inode, self.offset, buf.as_mut_ptr() as *mut libc::c_void, buf.len() as u32), "Couldn't read file content").map_err(|e| io::Error::new(io::ErrorKind::Other, e))? }; + self.offset += res as u64; + Ok(res as usize) + } + } +} + +impl<'a> Seek for File<'a> { + fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> { + let newoff = match pos { + io::SeekFrom::Start(off) => off as i64, + io::SeekFrom::End(off) => self.size() as i64 + off, + io::SeekFrom::Current(off) => self.offset as i64 + off, + }; + if newoff < 0 { + Err(io::Error::new(io::ErrorKind::Other, "Attempted to seek before beginning of file")) + } + else { + self.offset = newoff as u64; + Ok(self.offset) + } + } +} + +#[derive(Debug)] +pub enum Data<'a> { + File(File<'a>), + Dir(Dir<'a>), + Symlink(String), + BlockDev(u32), + CharDev(u32), + Fifo, + Socket, +} + +impl<'a> Data<'a> { + fn new(node: &'a Node) -> Result<Self> { + unsafe fn arr_to_string<'a, T>(arr: &bindings::__IncompleteArrayField<T>, len: usize) -> String { + let slice = std::slice::from_raw_parts(arr.as_ptr() as *const u8, len); + String::from_utf8_lossy(slice).into_owned() + } + match unsafe { (*node.inode).base.type_ } as u32 { + SQFS_INODE_TYPE_SQFS_INODE_DIR | SQFS_INODE_TYPE_SQFS_INODE_EXT_DIR => Ok(Self::Dir(Dir::new(node)?)), + SQFS_INODE_TYPE_SQFS_INODE_FILE | SQFS_INODE_TYPE_SQFS_INODE_EXT_FILE => Ok(Self::File(File::new(node)?)), + SQFS_INODE_TYPE_SQFS_INODE_SLINK => Ok(unsafe { + Self::Symlink(arr_to_string(&(*node.inode).extra, (*node.inode).data.slink.target_size as usize)) + }), + SQFS_INODE_TYPE_SQFS_INODE_EXT_SLINK => Ok(unsafe { + Self::Symlink(arr_to_string(&(*node.inode).extra, (*node.inode).data.slink_ext.target_size as usize)) + }), + SQFS_INODE_TYPE_SQFS_INODE_BDEV => Ok(unsafe { + Self::BlockDev((*node.inode).data.dev.devno) + }), + SQFS_INODE_TYPE_SQFS_INODE_EXT_BDEV => Ok(unsafe { + Self::BlockDev((*node.inode).data.dev_ext.devno) + }), + SQFS_INODE_TYPE_SQFS_INODE_CDEV => Ok(unsafe { + Self::CharDev((*node.inode).data.dev.devno) + }), + SQFS_INODE_TYPE_SQFS_INODE_EXT_CDEV => Ok(unsafe { + Self::CharDev((*node.inode).data.dev_ext.devno) + }), + SQFS_INODE_TYPE_SQFS_INODE_FIFO | SQFS_INODE_TYPE_SQFS_INODE_EXT_FIFO => Ok(Self::Fifo), + SQFS_INODE_TYPE_SQFS_INODE_SOCKET | SQFS_INODE_TYPE_SQFS_INODE_EXT_SOCKET => Ok(Self::Socket), + _ => Err(SquashfsError::LibraryReturnError("Unsupported inode type".to_string())), + } + } + + fn name(&self) -> String { + match self { + Data::File(_) => "regular file", + Data::Dir(_) => "directory", + Data::Symlink(_) => "symbolic link", + Data::BlockDev(_) => "block device", + Data::CharDev(_) => "character device", + Data::Fifo => "named pipe", + Data::Socket => "socket", + }.to_string() + } +} + +#[repr(u32)] +#[derive(Clone, Copy)] +pub enum XattrType { + User = SQFS_XATTR_TYPE_SQFS_XATTR_USER, + Trusted = SQFS_XATTR_TYPE_SQFS_XATTR_TRUSTED, + Security = SQFS_XATTR_TYPE_SQFS_XATTR_SECURITY, +} + +// TODO See how many of these instance variables (in all classes) we can make *const rather than *mut +// TODO Also check what should be clonable and whether that might share state unsafely +pub struct Node<'a> { + container: &'a Archive, + path: PathBuf, + inode: *const sqfs_inode_generic_t, +} + +impl<'a> Node<'a> { + fn new(container: &'a Archive, inode: *const sqfs_inode_generic_t, path: PathBuf) -> Result<Self> { + Ok(Self { container: container, path: path, inode: inode }) + } + + pub fn xattrs(&self, category: XattrType) -> Result<HashMap<Vec<u8>, Vec<u8>>> { + let xattr_reader = self.container.xattr_reader()?; + let mut xattr_idx: u32 = NO_XATTRS; + unsafe { sfs_check(sqfs_inode_get_xattr_index(self.inode, &mut xattr_idx), "Couldn't get xattr index")? }; + let desc = sfs_init(&|x| unsafe { + sqfs_xattr_reader_get_desc(xattr_reader, xattr_idx, x) + }, "Couldn't get xattr descriptor")?; + let mut ret: HashMap<Vec<u8>, Vec<u8>> = HashMap::new(); + unsafe { sfs_check(sqfs_xattr_reader_seek_kv(xattr_reader, &desc), "Couldn't seek to xattr location")? }; + for _ in 0..desc.count { + let prefixlen = unsafe { CStr::from_ptr(sqfs_get_xattr_prefix(category as u32)).to_bytes().len() }; + let key = sfs_init_ptr(&|x| unsafe { + sqfs_xattr_reader_read_key(xattr_reader, x) + }, "Couldn't read xattr key")?; + if unsafe { (*key).type_ } as u32 & SQFS_XATTR_TYPE_SQFS_XATTR_FLAG_OOL != 0 { + unimplemented!() + } + let val = sfs_init_ptr(&|x| unsafe { + sqfs_xattr_reader_read_value(xattr_reader, key, x) + }, "Couldn't read xattr value")?; + if unsafe { (*key).type_ } as u32 & SQFS_XATTR_TYPE_SQFS_XATTR_PREFIX_MASK == category as u32 { + unsafe { + let keyvec = (*key).key.as_slice((*key).size as usize + prefixlen)[prefixlen..].to_vec(); + let valvec = (*val).value.as_slice((*val).size as usize).to_vec(); + ret.insert(keyvec, valvec); + } + } + unsafe { + libc::free(val as *mut libc::c_void); + libc::free(key as *mut libc::c_void); + } + } + // FIXME Ensure this happens even in the case of an early return + unsafe { sfs_destroy(xattr_reader as *mut sqfs_object_t); } + Ok(ret) + } + + pub fn data(&self) -> Result<Data> { + Data::new(&self) + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn name(&self) -> String { + self.path.file_name().map(|x| x.to_string_lossy().to_string()).unwrap_or("/".to_string()) + } + + pub fn parent(&self) -> Result<Self> { + let ppath = self.path.parent().unwrap_or(&Path::new("")); + self.container.get(&path_to_string(ppath)?) + } + + pub fn resolve(&self) -> Result<Self> { + // TODO Error out if the link chain is more than 1000 elements + let mut visited = HashSet::new(); + let mut cur = Box::new(self.clone()); + loop { + match cur.data()? { + Data::Symlink(targetstr) => { + let target = cur.path.parent().unwrap_or(&Path::new("")).join(targetstr); + if !visited.insert(target.clone()) { + return Err(SquashfsError::LinkLoop(target)); + } + cur = Box::new(cur.container.getpath(&target)?); + } + _ => return Ok(*cur), + } + } + } + + pub fn as_file(&self) -> Result<File> { + match self.data()? { + Data::File(f) => Ok(f), + other => Err(SquashfsError::WrongType(self.path.to_path_buf(), other.name(), "regular file".to_string())), + } + } + + pub fn as_dir(&self) -> Result<Dir> { + match self.data()? { + Data::Dir(d) => Ok(d), + other => Err(SquashfsError::WrongType(self.path.to_path_buf(), other.name(), "directory".to_string())), + } + } +} + +impl<'a> std::clone::Clone for Node<'a> { + fn clone(&self) -> Self { + Self { container: self.container, path: self.path.clone(), inode: self.inode } + } +} + +impl<'a> std::fmt::Display for Node<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} at {}", self.data().unwrap(/*TODO*/).name(), self.path.display()) + } +} + +impl<'a> std::fmt::Debug for Node<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "node") + } +} + +pub struct Archive { + file: *mut sqfs_file_t, + superblock: Box<sqfs_super_t>, + compressor_config: Box<sqfs_compressor_config_t>, + compressor: *mut sqfs_compressor_t, + dir_reader: *mut sqfs_dir_reader_t, + root: *mut sqfs_inode_generic_t, +} + +impl Archive { + pub fn new(path: &str) -> Result<Self> { + let cpath = CString::new(path)?; + // FIXME If something errors halfway through initialization, we'll leak memory + let file = sfs_init_check_null(&|| unsafe { + sqfs_open_file(cpath.as_ptr(), SQFS_FILE_OPEN_FLAGS_SQFS_FILE_OPEN_READ_ONLY) + }, &format!("Couldn't open input file {}", path))?; + let superblock = Box::new(sfs_init(&|x| unsafe { + sqfs_super_read(x, file) + }, "Couldn't read archive superblock")?); + let compressor_config = Box::new(sfs_init(&|x| unsafe { + sqfs_compressor_config_init(x, superblock.compression_id as u32, superblock.block_size as u64, SQFS_COMP_FLAG_SQFS_COMP_FLAG_UNCOMPRESS as u16) + }, "Couldn't read archive compressor config")?); + let compressor = sfs_init_ptr(&|x| unsafe { + sqfs_compressor_create(&*compressor_config, x) + }, "Couldn't create compressor")?; + let dir_reader = sfs_init_check_null(&|| unsafe { + sqfs_dir_reader_create(&*superblock, compressor, file, 0) + }, "Couldn't create directory reader")?; + let root = sfs_init_ptr(&|x| unsafe { + sqfs_dir_reader_get_root_inode(dir_reader, x) + }, "Couldn't get filesystem root")?; + Ok(Self { file: file, superblock: superblock, compressor_config: compressor_config, compressor: compressor, dir_reader: dir_reader, root: root }) + } + + fn xattr_reader(&self) -> Result<*mut sqfs_xattr_reader_t> { + unsafe { + let ret = sqfs_xattr_reader_create(0); + sfs_check(sqfs_xattr_reader_load(ret, &*self.superblock, self.file, self.compressor), "Couldn't create xattr reader")?; + Ok(ret) + } + } + + + fn getpath(&self, path: &Path) -> Result<Node> { + let pathbuf = dumb_canonicalize(path); + if &pathbuf == Path::new("/") { Node::new(&self, self.root, pathbuf) } + else { + let cpath = CString::new(path_to_string(&pathbuf)?)?; + let inode = sfs_init_ptr(&|x| unsafe { + sqfs_dir_reader_find_by_path(self.dir_reader, self.root, cpath.as_ptr(), x) + }, &format!("Unable to access path {}", path.display()))?; + Node::new(&self, inode, pathbuf) + } + } + + pub fn get(&self, path: &str) -> Result<Node> { + self.getpath(Path::new(path)) + } +} + +impl Drop for Archive { + fn drop(&mut self) { + unsafe { + sfs_destroy(self.compressor as *mut sqfs_object_t); + sfs_destroy(self.file as *mut sqfs_object_t); + } + } +} + unsafe fn sfs_destroy(obj: *mut sqfs_object_t) { ((*obj).destroy.expect("Squashfs object did not provide a destory callback"))(obj); } -pub fn test() -> Result<()> { +fn oldtest() -> Result<()> { + fn sfs_err(desc: &str) -> Result<()> { + Err(SquashfsError::LibraryReturnError(desc.to_string())) + } let fname = "/home/matt/Scratch/wikivoyage.sfs"; unsafe { - let file = sqfs_open_file(CString::new(fname).map_err(|e| SquashfsError::NullInput(e))?.as_ptr(), SQFS_FILE_OPEN_FLAGS_SQFS_FILE_OPEN_READ_ONLY); + let file = sqfs_open_file(CString::new(fname)?.as_ptr(), SQFS_FILE_OPEN_FLAGS_SQFS_FILE_OPEN_READ_ONLY); if file.is_null() { sfs_err("Couldn't open input file")?; } - let superblock = { - let mut ret: MaybeUninit<sqfs_super_t> = MaybeUninit::uninit(); - sfs_check(sqfs_super_read(ret.as_mut_ptr(), file), "Couldn't read archive superblock")?; - ret.assume_init() - }; - let compressor_config = { - let mut ret: MaybeUninit<sqfs_compressor_config_t> = MaybeUninit::uninit(); - sqfs_compressor_config_init(ret.as_mut_ptr(), superblock.compression_id as u32, superblock.block_size as u64, SQFS_COMP_FLAG_SQFS_COMP_FLAG_UNCOMPRESS as u16); - ret.assume_init() - }; - let compressor = { - let mut ret: *mut sqfs_compressor_t = ptr::null_mut(); - sfs_check(sqfs_compressor_create(&compressor_config, &mut ret), "Couldn't create compressor")?; - if ret.is_null() { sfs_err("Couldn't create compressor")?; } - ret - }; + let superblock = sfs_init(&|x| sqfs_super_read(x, file), "Couldn't read archive superblock")?; + let compressor_config = sfs_init(&|x| sqfs_compressor_config_init(x, superblock.compression_id as u32, superblock.block_size as u64, SQFS_COMP_FLAG_SQFS_COMP_FLAG_UNCOMPRESS as u16), "Couldn't read archive compressor config")?; + let compressor = sfs_init_ptr(&|x| sqfs_compressor_create(&compressor_config, x), "Couldn't create compressor")?; let dir_reader = sqfs_dir_reader_create(&superblock, compressor, file, 0); if dir_reader.is_null() { sfs_err("Couldn't create directory reader")?; } let root = { @@ -112,7 +506,7 @@ pub fn test() -> Result<()> { } let inode = { let mut ret: *mut sqfs_inode_generic_t = ptr::null_mut(); - sfs_check(sqfs_dir_reader_find_by_path(dir_reader, root, CString::new("_meta/info.lua").map_err(|e| SquashfsError::NullInput(e))?.as_ptr(), &mut ret), "Couldn't find path")?; + sfs_check(sqfs_dir_reader_find_by_path(dir_reader, root, CString::new("_meta/info.lua")?.as_ptr(), &mut ret), "Couldn't find path")?; if ret.is_null() { sfs_err("Couldn't find path")?; } ret }; @@ -176,3 +570,24 @@ pub fn test() -> Result<()> { Ok(()) } } + +pub fn test() -> Result<()> { + let fname = "/home/matt/Scratch/wikivoyage.sfs"; + let a = Archive::new(fname)?; + let root = a.get("")?; + for entry in root.as_dir().unwrap() { + println!("{}", entry.name()); + } + let node = a.get("_meta/info.lua")?; + let mut file = node.as_file().unwrap(); + println!("File {} is {} bytes", node.path().display(), file.size()); + let mut content = "".to_string(); + file.read_to_string(&mut content).unwrap(); + println!("{}", content); + for (k, v) in node.xattrs(XattrType::User)? { + println!("{}: {}", String::from_utf8_lossy(&k), String::from_utf8_lossy(&v)); + } + let link = a.get("index.html")?; + println!("{} points to {}", link, link.resolve()?); + Ok(()) +} |