Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion src-tauri/src/game_artwork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,23 @@ impl ArtworkKind {
Self::ShortcutIcon => "game shortcut icon",
}
}

// Windows shortcuts need .ico files, so shortcut icons are stored as ICO.
fn formats(self) -> &'static [ImageFormat] {
match self {
Self::Icon | Self::Banner => &ImageFormat::ALL,
Self::ShortcutIcon => &SHORTCUT_ICON_FORMATS,
}
}
}

const SHORTCUT_ICON_FORMATS: [ImageFormat; 4] = [
ImageFormat::Png,
ImageFormat::Jpeg,
ImageFormat::Webp,
ImageFormat::Ico,
];

#[derive(Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GameArtworkResult {
Expand Down Expand Up @@ -281,7 +296,7 @@ fn existing_paths(
}

let mut paths = Vec::new();
for format in ImageFormat::ALL {
for &format in kind.formats() {
let path = artwork_path(directory, game_id, format);
match fs::symlink_metadata(&path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
Expand Down Expand Up @@ -509,6 +524,24 @@ mod tests {
fs::remove_dir_all(root).unwrap();
}

#[test]
fn shortcut_ico_is_found_by_path() {
let root = test_dir();
let store = GameArtworkStore::new(Ok(root.join("app-data")));
let id = "00000000-0000-0000-0000-000000000001";
let icon = crate::image_trim::shortcut_ico(&jpeg_image()).unwrap();
store.set_shortcut_icon(id, icon).unwrap();

let path = store.path(id, ArtworkKind::ShortcutIcon).unwrap().unwrap();
assert_eq!(
path,
root.join(format!("app-data/game-shortcut-icons/{id}.ico"))
);
store.remove(id, ArtworkKind::ShortcutIcon).unwrap();
assert_eq!(store.path(id, ArtworkKind::ShortcutIcon).unwrap(), None);
fs::remove_dir_all(root).unwrap();
}

#[test]
fn icon_is_persisted_and_returned_with_its_content_type() {
let root = test_dir();
Expand Down
44 changes: 43 additions & 1 deletion src-tauri/src/game_uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,27 @@ pub(crate) fn is_system_directory(path: &Path) -> bool {
/// Whether `path` lies in an operating system directory that Legio must never write to or delete.
#[cfg(windows)]
pub(crate) fn is_system_directory(path: &Path) -> bool {
std::env::var_os("SystemRoot").is_some_and(|system| path.starts_with(Path::new(&system)))
std::env::var_os("SystemRoot").is_some_and(|system| {
is_within_windows_directory(&path.to_string_lossy(), &system.to_string_lossy())
})
}

// Windows paths ignore case and accept both separators, which Path::starts_with does not.
#[cfg(any(windows, test))]
fn is_within_windows_directory(path: &str, directory: &str) -> bool {
let path = normalize_windows_path(path);
let directory = normalize_windows_path(directory);
path == directory
|| path
.strip_prefix(&directory)
.is_some_and(|rest| rest.starts_with('\\'))
}

#[cfg(any(windows, test))]
fn normalize_windows_path(path: &str) -> String {
path.replace('/', "\\")
.trim_end_matches('\\')
.to_lowercase()
}

fn remove_installation(
Expand Down Expand Up @@ -319,6 +339,28 @@ mod tests {
use crate::database::{CreateGameInput, database_error};
use rusqlite::params;

#[test]
fn system_directory_match() {
assert!(is_within_windows_directory(
r"C:\WINDOWS\System32\game.exe",
r"C:\Windows"
));
assert!(is_within_windows_directory(
"c:/windows/system32",
r"C:\Windows\"
));
assert!(is_within_windows_directory(r"C:\Windows", r"C:\WINDOWS"));
// Sibling directories that only share a name prefix are not system directories.
assert!(!is_within_windows_directory(
r"C:\WindowsApps\game.exe",
r"C:\Windows"
));
assert!(!is_within_windows_directory(
r"D:\Games\Windows",
r"C:\Windows"
));
}

struct Fixture {
root: PathBuf,
directory: PathBuf,
Expand Down
Loading