Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_pearlite/src/register.rs

7.4 KiB, 1 run

created by r1870400018:60982, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Registers the reader with the desktop for the current user: `.prl` documents get a type of their own,
2//! the reader becomes their default handler, and a launcher entry appears among the applications.
3//! Nothing is written outside the user's own profile, so no elevation is asked for.
4//!
5//! On Linux that is the freedesktop trio: a shared-mime-info package declaring `application/x-pearlite`,
6//! a `.desktop` entry whose `Exec` is this very executable, and `xdg-mime default`. On Windows it is the
7//! per-user `HKCU\Software\Classes` keys, written through `reg.exe` so that no Win32 call, and so no
8//! `unsafe`, is needed. Registration is idempotent, and is rerun after the executable moves.
9
10use oxedyne_fe2o3_core::prelude::*;
11
12use std::path::{
13 Path,
14 PathBuf,
15};
16use std::process::Command;
17
18pub const MIME_TYPE: &str = "application/x-pearlite";
19pub const DESKTOP_ID: &str = "pearlite-reader.desktop";
20pub const PROG_ID: &str = "Pearlite.Document";
21
22/// One action of a registration: a file to write, or a program to run. A plan is a list of these, built
23/// without touching the system, so what a registration would do is inspectable before it is done.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum Step {
26 Write {
27 path: PathBuf,
28 body: String,
29 },
30 Run {
31 prog: String,
32 args: Vec<String>,
33 required: bool, // a failure stops the registration, rather than being reported and passed
34 },
35}
36
37/// The registration a Linux desktop needs, rooted at the XDG data home `data_home` (normally
38/// `~/.local/share`), launching `exe`.
39pub fn plan_linux(exe: &Path, data_home: &Path) -> Vec<Step> {
40 let mime_dir = data_home.join("mime");
41 let apps_dir = data_home.join("applications");
42 let mime_xml = fmt!(concat!(
43 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
44 "<mime-info xmlns=\"http://www.freedesktop.org/standards/shared-mime-info\">\n",
45 " <mime-type type=\"{}\">\n",
46 " <comment>Pearlite document</comment>\n",
47 " <glob pattern=\"*.prl\"/>\n",
48 " <icon name=\"x-office-document\"/>\n",
49 " </mime-type>\n",
50 "</mime-info>\n"), MIME_TYPE);
51 let desktop = fmt!(concat!(
52 "[Desktop Entry]\n",
53 "Type=Application\n",
54 "Version=1.0\n",
55 "Name=Pearlite Reader\n",
56 "GenericName=Document Reader\n",
57 "Comment=Open and read Pearlite (.prl) documents\n",
58 "Exec={} %f\n",
59 "Icon=x-office-document\n",
60 "Terminal=false\n",
61 "Categories=Office;Viewer;\n",
62 "MimeType={};\n"), desktop_exec_arg(exe), MIME_TYPE);
63 vec![
64 Step::Write { path: mime_dir.join("packages").join("pearlite.xml"), body: mime_xml },
65 Step::Write { path: apps_dir.join(DESKTOP_ID), body: desktop },
66 // The two cache refreshes are what make the type and the entry visible at once; a desktop without
67 // the tools still picks both up at its next scan, so neither failure is fatal.
68 Step::Run {
69 prog: "update-mime-database".to_string(),
70 args: vec![mime_dir.to_string_lossy().into_owned()],
71 required: false,
72 },
73 Step::Run {
74 prog: "update-desktop-database".to_string(),
75 args: vec![apps_dir.to_string_lossy().into_owned()],
76 required: false,
77 },
78 Step::Run {
79 prog: "xdg-mime".to_string(),
80 args: vec!["default".to_string(), DESKTOP_ID.to_string(), MIME_TYPE.to_string()],
81 required: true,
82 },
83 ]
84}
85
86/// The per-user registry entries Windows needs, launching `exe`. Each is a `reg add … /f`, so a rerun
87/// overwrites rather than fails.
88pub fn plan_windows(exe: &Path) -> Vec<Step> {
89 let exe = exe.to_string_lossy().into_owned();
90 let classes = "HKCU\\Software\\Classes";
91 let prog = fmt!("{}\\{}", classes, PROG_ID);
92 let values: Vec<(String, Option<&str>, String)> = vec![ // (key, value name or default, data)
93 (fmt!("{}\\.prl", classes), None, PROG_ID.to_string()),
94 (fmt!("{}\\.prl", classes), Some("Content Type"), MIME_TYPE.to_string()),
95 (prog.clone(), None, "Pearlite document".to_string()),
96 (fmt!("{}\\DefaultIcon", prog), None, fmt!("\"{}\",0", exe)),
97 (fmt!("{}\\shell\\open\\command", prog), None, fmt!("\"{}\" \"%1\"", exe)),
98 ];
99 values.into_iter().map(|(key, name, data)| {
100 let mut args = vec!["add".to_string(), key];
101 match name {
102 Some(n) => { args.push("/v".to_string()); args.push(n.to_string()); },
103 None => args.push("/ve".to_string()),
104 }
105 args.extend(["/t".to_string(), "REG_SZ".to_string(), "/d".to_string(), data, "/f".to_string()]);
106 Step::Run { prog: "reg".to_string(), args, required: true }
107 }).collect()
108}
109
110/// A path as a `.desktop` `Exec` argument: always quoted, with the four characters the specification
111/// reserves inside quotes escaped, so a path with spaces or a dollar sign survives the launcher.
112pub fn desktop_exec_arg(exe: &Path) -> String {
113 let mut out = String::from("\"");
114 for c in exe.to_string_lossy().chars() {
115 if matches!(c, '"' | '`' | '$' | '\\') {
116 out.push('\\');
117 }
118 out.push(c);
119 }
120 out.push('"');
121 out
122}
123
124/// The executable a registration should point the desktop at. On Windows that is the console-free
125/// `pearlite-reader.exe` beside this binary when it is there, so a double-clicked document opens a
126/// window and no console; otherwise it is this binary itself.
127pub fn handler_exe() -> Outcome<PathBuf> {
128 let me = res!(std::env::current_exe(), IO, File);
129 if cfg!(windows) {
130 if let Some(dir) = me.parent() {
131 let gui = dir.join("pearlite-reader.exe");
132 if gui.is_file() {
133 return Ok(gui);
134 }
135 }
136 }
137 Ok(me)
138}
139
140/// The XDG data home: `$XDG_DATA_HOME` when set and absolute, else `$HOME/.local/share`.
141fn xdg_data_home() -> Outcome<PathBuf> {
142 if let Ok(d) = std::env::var("XDG_DATA_HOME") {
143 let p = PathBuf::from(d);
144 if p.is_absolute() {
145 return Ok(p);
146 }
147 }
148 let home = res!(std::env::var("HOME").map_err(|e| err!(e,
149 "Neither XDG_DATA_HOME nor HOME is set, so there is nowhere to register the reader."; Missing)));
150 Ok(PathBuf::from(home).join(".local").join("share"))
151}
152
153/// The plan for the platform this binary runs on.
154pub fn plan() -> Outcome<Vec<Step>> {
155 let exe = res!(handler_exe());
156 if cfg!(windows) {
157 Ok(plan_windows(&exe))
158 } else if cfg!(target_os = "linux") || cfg!(target_os = "freebsd") || cfg!(target_os = "openbsd") {
159 Ok(plan_linux(&exe, &res!(xdg_data_home())))
160 } else {
161 Err(err!("Registering the reader is not implemented on {}.", std::env::consts::OS;
162 Unimplemented))
163 }
164}
165
166/// Carries out a plan, returning one line per step saying what happened. A required step that fails
167/// stops the run with an error naming it; an optional one is reported and passed.
168pub fn execute(steps: &[Step]) -> Outcome<Vec<String>> {
169 let mut report = Vec::with_capacity(steps.len());
170 for step in steps {
171 match step {
172 Step::Write { path, body } => {
173 if let Some(dir) = path.parent() {
174 res!(std::fs::create_dir_all(dir), IO, File, Write);
175 }
176 res!(std::fs::write(path, body), IO, File, Write);
177 report.push(fmt!("wrote {}", path.display()));
178 },
179 Step::Run { prog, args, required } => {
180 let line = fmt!("{} {}", prog, args.join(" "));
181 let failure = match Command::new(prog).args(args).output() {
182 Ok(o) if o.status.success() => None,
183 Ok(o) => Some(fmt!("exited with {}: {}", o.status,
184 String::from_utf8_lossy(&o.stderr).trim())),
185 Err(e) => Some(fmt!("could not be started: {}", e)),
186 };
187 match (failure, required) {
188 (None, _) => report.push(fmt!("ran {}", line)),
189 (Some(f), true) => return Err(err!(
190 "Registration step `{}` {}.", line, f; IO, System)),
191 (Some(f), false) => report.push(fmt!("skipped {} ({})", line, f)),
192 }
193 },
194 }
195 }
196 Ok(report)
197}