use crate::error::{Error, Result};
use std::fs;
use std::path::PathBuf;
use std::time::SystemTime;
use tempfile::TempDir;
use zip::read::ZipArchive;
pub const DEFAULT_MAX_OMNI_SIZE: usize = 100 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct ExtractConfig {
pub max_omni_size: usize,
pub cache_dir: Option<PathBuf>,
pub target_files: Vec<String>,
pub force_refresh: bool,
}
impl Default for ExtractConfig {
fn default() -> Self {
Self {
max_omni_size: DEFAULT_MAX_OMNI_SIZE,
cache_dir: None,
target_files: vec![],
force_refresh: false,
}
}
}
pub struct OmniExtractor {
omni_path: PathBuf,
cache_dir: Option<TempDir>,
config: ExtractConfig,
}
impl OmniExtractor {
pub fn new(omni_path: PathBuf) -> Result<Self> {
Self::with_config(omni_path, ExtractConfig::default())
}
pub fn with_config(omni_path: PathBuf, config: ExtractConfig) -> Result<Self> {
if !omni_path.exists() {
return Err(Error::PrefFileNotFound {
file: omni_path.display().to_string(),
});
}
let metadata = fs::metadata(&omni_path)?;
let file_size = metadata.len() as usize;
if file_size > config.max_omni_size {
return Err(Error::OmniJaTooLarge {
actual: file_size,
limit: config.max_omni_size,
});
}
let cache_dir = if config.cache_dir.is_some() {
None
} else {
Some(TempDir::new()?)
};
Ok(Self {
omni_path,
cache_dir,
config,
})
}
pub fn extract_prefs(&self) -> Result<Vec<PathBuf>> {
if !self.config.force_refresh {
if let Ok(cached) = self.try_load_from_cache() {
return Ok(cached);
}
}
let extracted = self.extract_from_archive()?;
if let Err(e) = self.save_to_cache(&extracted) {
eprintln!("Warning: Failed to cache extracted files: {}", e);
}
Ok(extracted)
}
pub fn list_js_files(&self) -> Result<Vec<String>> {
match self.list_js_files_with_parser() {
Ok(files) => Ok(files),
Err(_) => {
self.list_js_files_with_unzip()
}
}
}
fn list_js_files_with_parser(&self) -> Result<Vec<String>> {
let file = fs::File::open(&self.omni_path)?;
let mut archive =
ZipArchive::new(file).map_err(|e| Error::ExtractionFailed(e.to_string()))?;
let mut js_files = Vec::new();
for i in 0..archive.len() {
let file = archive
.by_index(i)
.map_err(|e| Error::ExtractionFailed(e.to_string()))?;
let name = file.name().to_string();
if name.ends_with(".js") {
js_files.push(name);
}
}
Ok(js_files)
}
fn list_js_files_with_unzip(&self) -> Result<Vec<String>> {
let output = std::process::Command::new("unzip")
.arg("-l") .arg(&self.omni_path)
.output()
.map_err(|e| Error::ExtractionFailed(format!("unzip command failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::ExtractionFailed(format!(
"unzip command failed: {}",
stderr
)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut js_files = Vec::new();
for line in stdout.lines() {
if line.contains("length") || line.contains("---") || line.trim().is_empty() {
continue;
}
if let Some(last_space) = line.rfind(' ') {
let name = &line[last_space + 1..];
if name.ends_with(".js") {
js_files.push(name.to_string());
}
}
}
Ok(js_files)
}
fn extract_from_archive(&self) -> Result<Vec<PathBuf>> {
match self.extract_with_zip_parser() {
Ok(files) => Ok(files),
Err(_e) => {
self.extract_with_unzip_command()
}
}
}
fn extract_with_zip_parser(&self) -> Result<Vec<PathBuf>> {
let file = fs::File::open(&self.omni_path)?;
let mut archive =
ZipArchive::new(file).map_err(|e| Error::ExtractionFailed(e.to_string()))?;
let mut extracted_files = Vec::new();
let cache_dir = self.get_cache_path()?;
for i in 0..archive.len() {
let mut zipfile = archive
.by_index(i)
.map_err(|e| Error::ExtractionFailed(e.to_string()))?;
let name = zipfile.name().to_string();
if name.contains("..") || name.starts_with('/') || name.starts_with('\\') {
continue;
}
if self.should_extract(&name) {
let uncompressed_size = zipfile.size() as usize;
if zipfile.compressed_size() as usize > 0 && uncompressed_size > 10 * 1024 * 1024 {
continue;
}
let output_path = cache_dir.join(&name);
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
let mut output_file = fs::File::create(&output_path)?;
std::io::copy(&mut zipfile, &mut output_file)?;
extracted_files.push(output_path);
}
}
Ok(extracted_files)
}
fn extract_with_unzip_command(&self) -> Result<Vec<PathBuf>> {
let cache_dir = self.get_cache_path()?;
let output = std::process::Command::new("unzip")
.arg("-q") .arg("-o") .arg(&self.omni_path)
.arg("-d")
.arg(&cache_dir)
.output()
.map_err(|e| Error::ExtractionFailed(format!("unzip command failed: {}", e)))?;
let _status = output.status;
let mut extracted_files = Vec::new();
for entry in walkdir::WalkDir::new(&cache_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.is_file() && path.extension().map(|e| e == "js").unwrap_or(false) {
let rel_path = path
.strip_prefix(&cache_dir)
.map_err(|e| {
Error::ExtractionFailed(format!("Failed to get relative path: {}", e))
})?
.to_string_lossy()
.to_string();
if self.should_extract(&rel_path) {
extracted_files.push(path.to_path_buf());
} else {
let _ = fs::remove_file(path);
}
}
}
if extracted_files.is_empty() {
return Err(Error::ExtractionFailed(
"No .js files were extracted from omni.ja".to_string(),
));
}
Ok(extracted_files)
}
fn should_extract(&self, name: &str) -> bool {
if name.ends_with("/greprefs.js") || name == "greprefs.js" {
return true;
}
if self.config.target_files.is_empty() {
return name.ends_with(".js");
}
for pattern in &self.config.target_files {
if pattern.ends_with("*.js") {
let prefix = &pattern[..pattern.len() - 4]; if name.starts_with(prefix) && name.ends_with(".js") {
return true;
}
} else if name == pattern {
return true;
}
}
false
}
fn try_load_from_cache(&self) -> Result<Vec<PathBuf>> {
let cache_dir = self.get_cache_path()?;
if !cache_dir.exists() {
return Err(Error::ExtractionFailed("Cache not found".to_string()));
}
let cache_metadata = fs::metadata(&cache_dir)?;
let omni_metadata = fs::metadata(&self.omni_path)?;
if let (Ok(cache_time), Ok(omni_time)) =
(cache_metadata.modified(), omni_metadata.modified())
{
if cache_time < omni_time {
return Err(Error::ExtractionFailed("Cache is stale".to_string()));
}
}
let mut cached_files = Vec::new();
for entry in walkdir::WalkDir::new(&cache_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.is_file() && path.extension().map(|e| e == "js").unwrap_or(false) {
cached_files.push(path.to_path_buf());
}
}
if cached_files.is_empty() {
return Err(Error::ExtractionFailed("No cached files".to_string()));
}
Ok(cached_files)
}
fn save_to_cache(&self, _files: &[PathBuf]) -> Result<()> {
Ok(())
}
fn get_cache_path(&self) -> Result<PathBuf> {
if let Some(ref custom_dir) = self.config.cache_dir {
Ok(custom_dir.clone())
} else if let Some(ref temp_dir) = self.cache_dir {
Ok(temp_dir.path().to_path_buf())
} else {
let mut cache_path = std::env::temp_dir();
cache_path.push("ffcv");
cache_path.push("omni");
cache_path.push(format!(
"{}_{}",
self.omni_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("omni"),
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
));
fs::create_dir_all(&cache_path)?;
Ok(cache_path)
}
}
pub fn clear_cache(&self) -> Result<()> {
let cache_dir = self.get_cache_path()?;
if cache_dir.exists() {
fs::remove_dir_all(&cache_dir)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_config_default() {
let config = ExtractConfig::default();
assert_eq!(config.max_omni_size, DEFAULT_MAX_OMNI_SIZE);
assert!(!config.force_refresh);
assert!(config.target_files.is_empty());
}
#[test]
fn test_should_extract_patterns() {
let config = ExtractConfig {
target_files: vec!["defaults/pref/*.js".to_string()],
..Default::default()
};
let extractor = OmniExtractor {
omni_path: PathBuf::from("/fake/omni.ja"),
cache_dir: None,
config,
};
assert!(extractor.should_extract("defaults/pref/browser.js"));
assert!(extractor.should_extract("defaults/pref/firefox.js"));
assert!(!extractor.should_extract("defaults/pref/readme.txt")); assert!(!extractor.should_extract("other/file.js"));
let config2 = ExtractConfig {
target_files: vec!["greprefs.js".to_string()],
..Default::default()
};
let extractor2 = OmniExtractor {
omni_path: PathBuf::from("/fake/omni.ja"),
cache_dir: None,
config: config2,
};
assert!(extractor2.should_extract("greprefs.js"));
assert!(!extractor2.should_extract("other.js"));
}
}