1use std::{
2 env,
3 ffi::OsStr,
4 fs::{self, File},
5 io::{BufReader, BufWriter, Write},
6 path::{Path, PathBuf},
7};
8
9use image::{imageops::FilterType, DynamicImage, GenericImageView};
10use regex::Regex;
11use soar_dl::downloader::{DownloadOptions, Downloader};
12use soar_dl::utils::FileMode;
13
14use crate::{
15 config::get_config,
16 constants::PNG_MAGIC_BYTES,
17 database::models::{Package, PackageExt},
18 error::{ErrorContext, SoarError},
19 utils::{calc_magic_bytes, create_symlink, home_data_path, process_dir},
20 SoarResult,
21};
22
23use super::{
24 appimage::integrate_appimage, get_file_type, wrappe::setup_wrappe_portable_dir, PackageFormat,
25};
26
27const SUPPORTED_DIMENSIONS: &[(u32, u32)] = &[
28 (16, 16),
29 (24, 24),
30 (32, 32),
31 (48, 48),
32 (64, 64),
33 (72, 72),
34 (80, 80),
35 (96, 96),
36 (128, 128),
37 (192, 192),
38 (256, 256),
39 (512, 512),
40];
41
42fn find_nearest_supported_dimension(width: u32, height: u32) -> (u32, u32) {
43 SUPPORTED_DIMENSIONS
44 .iter()
45 .min_by_key(|&&(w, h)| {
46 let width_diff = (w as i32 - width as i32).abs();
47 let height_diff = (h as i32 - height as i32).abs();
48 width_diff + height_diff
49 })
50 .cloned()
51 .unwrap_or((width, height))
52}
53
54fn normalize_image(image: DynamicImage) -> DynamicImage {
55 let (width, height) = image.dimensions();
56 let (new_width, new_height) = find_nearest_supported_dimension(width, height);
57
58 if (width, height) != (new_width, new_height) {
59 image.resize(new_width, new_height, FilterType::Lanczos3)
60 } else {
61 image
62 }
63}
64
65pub fn symlink_icon<P: AsRef<Path>>(real_path: P) -> SoarResult<PathBuf> {
66 let real_path = real_path.as_ref();
67 let icon_name = real_path.file_stem().unwrap();
68 let ext = real_path.extension();
69
70 let (w, h) = if ext == Some(OsStr::new("svg")) {
71 (128, 128)
72 } else {
73 let image = image::open(real_path)?;
74 let (orig_w, orig_h) = image.dimensions();
75
76 let normalized_image = normalize_image(image);
77 let (w, h) = normalized_image.dimensions();
78
79 if (w, h) != (orig_w, orig_h) {
80 normalized_image.save(real_path)?;
81 }
82
83 (w, h)
84 };
85
86 let final_path = PathBuf::from(format!(
87 "{}/icons/hicolor/{w}x{h}/apps/{}-soar.{}",
88 home_data_path(),
89 icon_name.to_string_lossy(),
90 ext.unwrap_or_default().to_string_lossy()
91 ));
92
93 create_symlink(real_path, &final_path)?;
94 Ok(final_path)
95}
96
97pub fn symlink_desktop<P: AsRef<Path>, T: PackageExt>(
98 real_path: P,
99 package: &T,
100) -> SoarResult<PathBuf> {
101 let pkg_name = package.pkg_name();
102 let real_path = real_path.as_ref();
103 let content = fs::read_to_string(real_path)
104 .with_context(|| format!("reading content of desktop file: {}", real_path.display()))?;
105 let file_name = real_path.file_stem().unwrap();
106
107 let final_content = {
108 let re = Regex::new(r"(?m)^(Icon|Exec|TryExec)=(.*)").unwrap();
109
110 re.replace_all(&content, |caps: ®ex::Captures| match &caps[1] {
111 "Icon" => format!("Icon={}-soar", file_name.to_string_lossy()),
112 "Exec" | "TryExec" => {
113 let value = &caps[0];
114 let bin_path = get_config().get_bin_path().unwrap();
115 let new_value = format!("{}/{}", &bin_path.display(), pkg_name);
116
117 if value.contains("{{pkg_path}}") {
118 value.replace("{{pkg_path}}", &new_value)
119 } else {
120 format!("{}={}", &caps[1], new_value)
121 }
122 }
123 _ => unreachable!(),
124 })
125 .to_string()
126 };
127
128 let mut writer = BufWriter::new(
129 File::create(real_path)
130 .with_context(|| format!("creating desktop file {}", real_path.display()))?,
131 );
132 writer
133 .write_all(final_content.as_bytes())
134 .with_context(|| format!("writing desktop file to {}", real_path.display()))?;
135
136 let final_path = PathBuf::from(format!(
137 "{}/applications/{}-soar.desktop",
138 home_data_path(),
139 file_name.to_string_lossy()
140 ));
141
142 create_symlink(real_path, &final_path)?;
143 Ok(final_path)
144}
145
146pub async fn integrate_remote<P: AsRef<Path>>(
147 package_path: P,
148 package: &Package,
149) -> SoarResult<()> {
150 let package_path = package_path.as_ref();
151 let icon_url = &package.icon;
152 let desktop_url = &package.desktop;
153
154 let mut icon_output_path = package_path.join(".DirIcon");
155 let desktop_output_path = package_path.join(format!("{}.desktop", package.pkg_name));
156
157 let downloader = Downloader::default();
158
159 if let Some(icon_url) = icon_url {
160 let options = DownloadOptions {
161 url: icon_url.clone(),
162 output_path: Some(icon_output_path.to_string_lossy().to_string()),
163 progress_callback: None,
164 extract_archive: false,
165 extract_dir: None,
166 file_mode: FileMode::SkipExisting,
167 prompt: None,
168 };
169 downloader.download(options).await?;
170
171 let ext = if calc_magic_bytes(icon_output_path, 8)? == PNG_MAGIC_BYTES {
172 "png"
173 } else {
174 "svg"
175 };
176 icon_output_path = package_path.join(format!("{}.{}", package.pkg_name, ext));
177 }
178
179 if let Some(desktop_url) = desktop_url {
180 let options = DownloadOptions {
181 url: desktop_url.clone(),
182 output_path: Some(desktop_output_path.to_string_lossy().to_string()),
183 progress_callback: None,
184 extract_archive: false,
185 extract_dir: None,
186 file_mode: FileMode::SkipExisting,
187 prompt: None,
188 };
189 downloader.download(options).await?;
190 } else {
191 let content = create_default_desktop_entry(&package.pkg_name, "Utility");
192 fs::write(&desktop_output_path, &content).with_context(|| {
193 format!("writing to desktop file {}", desktop_output_path.display())
194 })?;
195 }
196
197 symlink_icon(&icon_output_path)?;
198 symlink_desktop(&desktop_output_path, package)?;
199
200 Ok(())
201}
202
203pub fn create_portable_link<P: AsRef<Path>>(
204 portable_path: P,
205 real_path: P,
206 pkg_name: &str,
207 extension: &str,
208) -> SoarResult<()> {
209 let base_dir = env::current_dir()
210 .map_err(|_| SoarError::Custom("Error retrieving current directory".into()))?;
211 let portable_path = portable_path.as_ref();
212 let portable_path = if portable_path.is_absolute() {
213 portable_path
214 } else {
215 &base_dir.join(portable_path)
216 };
217 let portable_path = portable_path.join(pkg_name).with_extension(extension);
218
219 fs::create_dir_all(&portable_path)
220 .with_context(|| format!("creating directory {}", portable_path.display()))?;
221 create_symlink(&portable_path, &real_path.as_ref().to_path_buf())?;
222 Ok(())
223}
224
225pub fn setup_portable_dir<P: AsRef<Path>, T: PackageExt>(
226 bin_path: P,
227 package: &T,
228 portable: Option<&str>,
229 portable_home: Option<&str>,
230 portable_config: Option<&str>,
231 portable_share: Option<&str>,
232 portable_cache: Option<&str>,
233) -> SoarResult<()> {
234 let portable_dir_base = get_config().get_portable_dirs()?.join(format!(
235 "{}-{}",
236 package.pkg_name(),
237 package.pkg_id()
238 ));
239 let bin_path = bin_path.as_ref();
240
241 let pkg_name = package.pkg_name();
242 let pkg_config = bin_path.with_extension("config");
243 let pkg_home = bin_path.with_extension("home");
244 let pkg_share = bin_path.with_extension("share");
245 let pkg_cache = bin_path.with_extension("cache");
246
247 let (portable_home, portable_config, portable_share, portable_cache) =
248 if let Some(portable) = portable {
249 (
250 Some(portable),
251 Some(portable),
252 Some(portable),
253 Some(portable),
254 )
255 } else {
256 (
257 portable_home,
258 portable_config,
259 portable_share,
260 portable_cache,
261 )
262 };
263
264 for (opt, target, kind) in [
265 (portable_home, &pkg_home, "home"),
266 (portable_config, &pkg_config, "config"),
267 (portable_share, &pkg_share, "share"),
268 (portable_cache, &pkg_cache, "cache"),
269 ] {
270 if let Some(val) = opt {
271 let base = if val.is_empty() {
272 &portable_dir_base
273 } else {
274 Path::new(val)
275 };
276 create_portable_link(base, target, pkg_name, kind)?;
277 }
278 }
279
280 Ok(())
281}
282
283fn create_default_desktop_entry(name: &str, categories: &str) -> Vec<u8> {
284 format!(
285 "[Desktop Entry]\n\
286 Type=Application\n\
287 Name={name}\n\
288 Icon={name}\n\
289 Exec={name}\n\
290 Categories={categories};\n",
291 )
292 .as_bytes()
293 .to_vec()
294}
295
296pub async fn integrate_package<P: AsRef<Path>, T: PackageExt>(
297 install_dir: P,
298 package: &T,
299 portable: Option<&str>,
300 portable_home: Option<&str>,
301 portable_config: Option<&str>,
302 portable_share: Option<&str>,
303 portable_cache: Option<&str>,
304) -> SoarResult<()> {
305 let install_dir = install_dir.as_ref();
306 let pkg_name = package.pkg_name();
307 let bin_path = install_dir.join(pkg_name);
308
309 let mut has_desktop = false;
310 let mut has_icon = false;
311 let mut symlink_action = |path: &Path| -> SoarResult<()> {
312 let ext = path.extension();
313 if ext == Some(OsStr::new("desktop")) {
314 has_desktop = true;
315 symlink_desktop(path, package)?;
316 }
317 Ok(())
318 };
319 process_dir(install_dir, &mut symlink_action)?;
320
321 let mut symlink_action = |path: &Path| -> SoarResult<()> {
322 let ext = path.extension();
323 if ext == Some(OsStr::new("png")) || ext == Some(OsStr::new("svg")) {
324 has_icon = true;
325 symlink_icon(path)?;
326 }
327 Ok(())
328 };
329 process_dir(install_dir, &mut symlink_action)?;
330
331 let mut reader = BufReader::new(
332 File::open(&bin_path).with_context(|| format!("opening {}", bin_path.display()))?,
333 );
334 let file_type = get_file_type(&mut reader)?;
335
336 match file_type {
337 PackageFormat::AppImage | PackageFormat::RunImage => {
338 if matches!(file_type, PackageFormat::AppImage) {
339 let _ = integrate_appimage(install_dir, &bin_path, package, has_icon, has_desktop)
340 .await;
341 }
342 setup_portable_dir(
343 bin_path,
344 package,
345 portable,
346 portable_home,
347 portable_config,
348 portable_share,
349 portable_cache,
350 )?;
351 }
352 PackageFormat::FlatImage => {
353 setup_portable_dir(
354 format!("{}/.{}", bin_path.parent().unwrap().display(), pkg_name),
355 package,
356 None,
357 None,
358 portable_config,
359 None,
360 None,
361 )?;
362 }
363 PackageFormat::Wrappe => {
364 setup_wrappe_portable_dir(&bin_path, pkg_name, portable)?;
365 }
366 _ => {}
367 }
368
369 Ok(())
370}