firefox_rs/
lib.rs

1mod util;
2
3use std::{env::temp_dir, fs::File, io::Write, process::Command};
4
5use lz4_flex::block::DecompressError;
6use util::{decompress_lz4, list_recovery_files};
7
8/// Crate global errors
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11    /// Firefox data folder or needed files inside are not accessible
12    #[error("Firefox data dir not found: {0}")]
13    FFDirNotFound(&'static str),
14    /// Std IO error
15    #[error("I/O error: {0}")]
16    Io(#[from] std::io::Error),
17    /// Failure to decompress lz4; e.g. recovery.json
18    #[error("LZ4 error: {0}")]
19    Lz4Decompression(#[from] DecompressError),
20    /// Json ser/de error
21    #[error("JSON error: {0}")]
22    Json(#[from] serde_json::Error),
23    /// Composed error; e.g. if list_tabs() failed trying multiple recovery files
24    #[error("Multiple errors: {0}")]
25    Multi(String),
26    #[error("Subcommand failed")]
27    ExitStatus,
28}
29
30/// Firefox Result
31pub type FFResult<T> = Result<T, Error>;
32
33mod recovery {
34    use serde::Deserialize;
35
36    #[derive(Deserialize, Debug)]
37    pub struct TopLevel {
38        pub windows: Vec<Window>,
39    }
40
41    #[derive(Deserialize, Debug)]
42    pub struct Window {
43        pub tabs: Vec<Tab>,
44    }
45
46    #[derive(Deserialize, Debug)]
47    pub struct Tab {
48        entries: Vec<Entry>,
49        index: usize,
50        pub image: Option<String>,
51    }
52
53    impl Tab {
54        pub fn into_entry(mut self) -> Entry {
55            self.entries.swap_remove(self.index - 1)
56        }
57    }
58
59    #[derive(Deserialize, Debug)]
60    pub struct Entry {
61        pub title: String,
62        pub url: String,
63    }
64}
65
66/// Firefox tab representation
67#[derive(Debug)]
68pub struct Tab {
69    /// Tab's title
70    pub title: String,
71    /// Tab's url
72    pub url: String,
73    /// Tab's icon
74    pub icon: Option<String>,
75}
76
77impl From<recovery::Tab> for Tab {
78    fn from(mut t: recovery::Tab) -> Self {
79        let icon = t.image.take();
80        let recovery::Entry { title, url } = t.into_entry();
81        Tab { title, url, icon }
82    }
83}
84
85impl Tab {
86    /// Try to focus this tab using hack.
87    ///
88    /// Firefox extension: [focusTab](https://addons.mozilla.org/en-US/firefox/addon/focus_tab/) is required
89    pub fn focus(&self) -> FFResult<()> {
90        let hack = format!(
91            "<!DOCTYPE html><body>\
92            <script>window.focusTab({{url:'{}'}});\
93            open(location, '_self').close();\
94            </script></body></html>",
95            self.url
96        );
97        let path = temp_dir().join("firefox-rs-focus.html");
98        let mut f = File::create(&path)?;
99        f.write_all(hack.as_bytes())?;
100        let mut child = Command::new("firefox").arg(path).spawn()?;
101        if !child.wait()?.success() {
102            return Err(Error::ExitStatus);
103        }
104        Ok(())
105    }
106}
107
108/// Returns list of tabs in open firefox instance
109pub fn list_tabs() -> FFResult<Vec<Tab>> {
110    let mut errors = Vec::with_capacity(0);
111    // in case of multi error; add errors accumulated in iterations to error vec
112    macro_rules! try_add {
113        ($result:expr) => {
114            match $result {
115                Ok(ok) => ok,
116                Err(e) => {
117                    errors.push(Error::from(e));
118                    continue;
119                }
120            }
121        };
122    }
123    for path_res in list_recovery_files()? {
124        let path = path_res?;
125
126        // decompression and deserialization are errors that cause to skip this path
127        // -- not causing to cancel list_tabs()
128        let buf = try_add!(decompress_lz4(path));
129        let topl: recovery::TopLevel = try_add!(serde_json::from_slice(&buf));
130
131        // this should be error free
132        // TODO: if index is out of bounds in recovery.json -- this crashes
133        let tabs = topl
134            .windows
135            .into_iter()
136            .flat_map(|window| window.tabs)
137            .map(Tab::from)
138            .collect();
139        return Ok(tabs);
140    }
141
142    // TODO: is this really necessary? are there more than one "recovery.json" to worry about?
143    match errors.len() {
144        0 => Err(Error::FFDirNotFound("recovery.json*")),
145        1 => Err(errors.swap_remove(0)),
146        _ => Err(Error::Multi({
147            let mut errors_s = String::new();
148            for (i, e) in errors.into_iter().enumerate() {
149                errors_s += &format!("({i}) {e} ");
150            }
151            errors_s
152        })),
153    }
154}
155
156#[cfg(test)]
157mod tests {}