Initial commit
This commit is contained in:
commit
296b987e3b
14 files changed
+4035
No files matched your search
+150
@@ -0,0 +1,150 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsStr,
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use backhand::{FilesystemReader, InnerNode, SquashfsFileReader};
|
||||
|
||||
use crate::desktop::DesktopEntry;
|
||||
|
||||
pub struct Icon {
|
||||
pub data: Vec<u8>,
|
||||
pub ext: &'static str,
|
||||
}
|
||||
|
||||
pub struct Embedded {
|
||||
pub desktop: Option<DesktopEntry>,
|
||||
pub icon: Option<Icon>,
|
||||
}
|
||||
|
||||
pub fn read(path: &Path, offset: u64) -> Result<Embedded> {
|
||||
let file = BufReader::new(File::open(path)?);
|
||||
let fs = FilesystemReader::from_reader_with_offset(file, offset).with_context(|| {
|
||||
format!(
|
||||
"cannot read the squashfs image embedded in {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let image = Image::new(&fs);
|
||||
let desktop = image.root_desktop();
|
||||
let icon_name = desktop.as_ref().and_then(|d| d.get("Icon"));
|
||||
let icon = image.icon(icon_name.as_deref());
|
||||
Ok(Embedded { desktop, icon })
|
||||
}
|
||||
|
||||
struct Image<'a, 'b> {
|
||||
fs: &'a FilesystemReader<'b>,
|
||||
nodes: HashMap<PathBuf, &'a InnerNode<SquashfsFileReader>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Image<'a, 'b> {
|
||||
fn new(fs: &'a FilesystemReader<'b>) -> Self {
|
||||
let nodes = fs
|
||||
.files()
|
||||
.map(|node| (normalize(&node.fullpath), &node.inner))
|
||||
.collect();
|
||||
Self { fs, nodes }
|
||||
}
|
||||
|
||||
fn read(&self, path: &Path) -> Option<Vec<u8>> {
|
||||
let mut path = normalize(path);
|
||||
for _ in 0..16 {
|
||||
match self.nodes.get(&path)? {
|
||||
InnerNode::File(file) => {
|
||||
let mut data = Vec::new();
|
||||
self.fs.file(file).reader().read_to_end(&mut data).ok()?;
|
||||
return Some(data);
|
||||
}
|
||||
InnerNode::Symlink(symlink) => {
|
||||
let parent = path.parent().unwrap_or(Path::new(""));
|
||||
path = normalize(&parent.join(&symlink.link));
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn root_desktop(&self) -> Option<DesktopEntry> {
|
||||
let mut candidates: Vec<&PathBuf> = self
|
||||
.nodes
|
||||
.keys()
|
||||
.filter(|p| p.components().count() == 1 && p.extension() == Some(OsStr::new("desktop")))
|
||||
.collect();
|
||||
candidates.sort();
|
||||
let data = self.read(candidates.first()?)?;
|
||||
Some(DesktopEntry::parse(&String::from_utf8_lossy(&data)))
|
||||
}
|
||||
|
||||
fn icon(&self, name: Option<&str>) -> Option<Icon> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(name) = name.filter(|n| !n.is_empty()) {
|
||||
let name = Path::new(name)
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or(name);
|
||||
candidates.push(PathBuf::from(name));
|
||||
for ext in ["svg", "png", "xpm"] {
|
||||
candidates.push(PathBuf::from(format!("{name}.{ext}")));
|
||||
}
|
||||
candidates.extend(self.hicolor(name));
|
||||
}
|
||||
candidates.push(PathBuf::from(".DirIcon"));
|
||||
candidates
|
||||
.iter()
|
||||
.find_map(|path| self.read(path).and_then(icon_from))
|
||||
}
|
||||
|
||||
fn hicolor(&self, name: &str) -> Vec<PathBuf> {
|
||||
let mut found: Vec<(u32, PathBuf)> = self
|
||||
.nodes
|
||||
.keys()
|
||||
.filter_map(|path| {
|
||||
let rest = path.strip_prefix("usr/share/icons/hicolor").ok()?;
|
||||
if path.file_stem()? != OsStr::new(name) {
|
||||
return None;
|
||||
}
|
||||
let size = rest.components().next()?.as_os_str().to_str()?;
|
||||
let rank = if size == "scalable" {
|
||||
u32::MAX
|
||||
} else {
|
||||
size.split('x').next()?.parse().ok()?
|
||||
};
|
||||
Some((rank, path.clone()))
|
||||
})
|
||||
.collect();
|
||||
found.sort_by_key(|(rank, _)| std::cmp::Reverse(*rank));
|
||||
found.into_iter().map(|(_, path)| path).collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_from(data: Vec<u8>) -> Option<Icon> {
|
||||
let ext = if data.starts_with(b"\x89PNG") {
|
||||
"png"
|
||||
} else if data.starts_with(b"/* XPM */") {
|
||||
"xpm"
|
||||
} else if String::from_utf8_lossy(&data[..data.len().min(4096)]).contains("<svg") {
|
||||
"svg"
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(Icon { data, ext })
|
||||
}
|
||||
|
||||
fn normalize(path: &Path) -> PathBuf {
|
||||
let mut out = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Normal(part) => out.push(part),
|
||||
Component::ParentDir => {
|
||||
out.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in new issue
Block a user