1
Fork 0
mirror of https://github.com/redstrate/Miscel.git synced 2025-05-20 12:07:46 +00:00

Add initial files

This commit is contained in:
Joshua Goins 2025-04-28 22:58:36 -04:00
commit 7b08f54bb6
7 changed files with 676 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

198
Cargo.lock generated Normal file
View file

@ -0,0 +1,198 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cfg-expr"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miscel"
version = "0.1.0"
dependencies = [
"system-deps",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "system-deps"
version = "7.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml",
"version-compare",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "toml"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "version-compare"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
[[package]]
name = "winnow"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5"
dependencies = [
"memchr",
]

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "miscel"
version = "0.1.0"
edition = "2024"
[build-dependencies]
system-deps = "7"
[package.metadata.system-deps]
libunshield = { version = "1.4", feature = "game_install" }
[features]
default = []
# enables game installation support using unshield (only supported on Linux and macOS)
game_install = []
[dependencies]

63
src/execlookup.rs Normal file
View file

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
use std::fs;
fn from_u16(from: &mut [u16]) -> &[u8] {
#[cfg(target_endian = "little")]
from.iter_mut().for_each(|word| *word = word.to_be());
let ptr: *const u8 = from.as_ptr().cast();
let len = from.len().checked_mul(2).unwrap();
unsafe { std::slice::from_raw_parts(ptr, len) }
}
fn find_needle(installer_file: &[u8], needle: &str) -> Option<String> {
let mut needle: Vec<u16> = needle.encode_utf16().collect();
let bytes = from_u16(&mut needle);
let mut position = installer_file
.windows(bytes.len())
.position(|window| window == bytes)?;
let parse_char_at_position = |position: usize| {
let upper = installer_file[position];
let lower = installer_file[position + 1];
let result = char::decode_utf16([((upper as u16) << 8) | lower as u16])
.map(|r| r.map_err(|e| e.unpaired_surrogate()))
.collect::<Vec<_>>();
result[0]
};
let mut string: String = String::new();
let mut last_char = parse_char_at_position(position);
while last_char.is_ok() && last_char.unwrap() != '\0' {
string.push(last_char.unwrap());
position += 2;
last_char = parse_char_at_position(position);
}
Some(string)
}
/// Extract the frontier URL from ffxivlauncher.exe
pub fn extract_frontier_url(launcher_path: &str) -> Option<String> {
let installer_file = fs::read(launcher_path).unwrap();
// New Frontier URL format
if let Some(url) = find_needle(&installer_file, "https://launcher.finalfantasyxiv.com") {
return Some(url);
}
// Old Frontier URL format
if let Some(url) = find_needle(&installer_file, "https://frontier.ffxiv.com") {
return Some(url);
}
None
}

239
src/existing_dirs.rs Normal file
View file

@ -0,0 +1,239 @@
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
// Rust deprecating this is stupid, I don't want to use a crate here
#[allow(deprecated)]
use std::env::home_dir;
use std::fs;
use std::fs::read_dir;
use std::path::PathBuf;
/// Where the existing installation came from
#[derive(Clone, Copy)]
#[repr(C)]
pub enum ExistingInstallType {
/// Installed via the official launcher
OfficialLauncher,
/// Installed via XIVQuickLauncher
XIVQuickLauncher,
/// Installed via XIVLauncherCore
XIVLauncherCore,
/// Installed via XIVOnMac
XIVOnMac,
/// Installed via Astra
Astra,
}
/// An existing install location on disk
pub struct ExistingGameDirectory {
/// The application where this installation was from
pub install_type: ExistingInstallType,
/// The path to the "main folder" where "game" and "boot" sits
pub path: String,
}
/// Finds existing installations on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
pub fn find_existing_game_dirs() -> Vec<ExistingGameDirectory> {
let mut install_dirs = Vec::new();
match std::env::consts::OS {
"linux" => {
// Official install (Wine)
install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir(".wine/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn")
});
// Official install (Steam)
install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir(
".steam/steam/steamapps/common/FINAL FANTASY XIV - A Realm Reborn",
),
});
// XIVLauncherCore location
install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::XIVLauncherCore,
path: from_home_dir(".xlcore/ffxiv"),
});
// Astra location. But we have to iterate through each UUID.
if let Ok(entries) = read_dir(from_home_dir(".local/share/astra/game/")) {
entries
.flatten()
.flat_map(|entry| {
let Ok(meta) = entry.metadata() else {
return vec![];
};
if meta.is_dir() {
return vec![entry.path()];
}
vec![]
})
.for_each(|path| {
install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::Astra,
path: path.into_os_string().into_string().unwrap(),
})
});
}
}
"macos" => {
// Official Launcher (macOS)
install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir("Library/Application Support/FINAL FANTASY XIV ONLINE/Bottles/published_Final_Fantasy/drive_c/Program Files (x86)/SquareEnix/FINAL FANTASY XIV - A Realm Reborn")
});
// TODO: add XIV on Mac
}
"windows" => {
// Official install (Wine)
install_dirs.push(ExistingGameDirectory {
install_type: ExistingInstallType::OfficialLauncher,
path: "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn"
.parse()
.unwrap(),
});
// TODO: Add Astra
}
&_ => {}
}
install_dirs
.into_iter()
.filter(|dir| is_valid_game_dir(&dir.path))
.collect()
}
/// An existing user directory
pub struct ExistingUserDirectory {
/// The application where this directory was from
pub install_type: ExistingInstallType,
/// The path to the user folder
pub path: String,
}
/// Finds existing user folders on disk. Will only return locations that actually have files in them, and a really basic check to see if the data is valid.
pub fn find_existing_user_dirs() -> Vec<ExistingUserDirectory> {
let mut user_dirs = Vec::new();
#[allow(deprecated)] // We still want std::env::home_dir
let Some(_) = home_dir() else {
return user_dirs;
};
match std::env::consts::OS {
"linux" => {
// Official install (Wine)
user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
});
// XIVLauncherCore location
user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::XIVLauncherCore,
path: from_home_dir(".xlcore/ffxivConfig"),
});
// Astra location. But we have to iterate through each UUID.
if let Ok(entries) = read_dir(from_home_dir(".local/share/astra/user/")) {
entries
.flatten()
.flat_map(|entry| {
let Ok(meta) = entry.metadata() else {
return vec![];
};
if meta.is_dir() {
return vec![entry.path()];
}
vec![]
})
.for_each(|path| {
user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::Astra,
path: path.into_os_string().into_string().unwrap(),
})
});
}
}
"macos" => {
// Official install (Wine)
user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
})
// TODO: Add XIV on Mac?
}
"windows" => {
// Official install
user_dirs.push(ExistingUserDirectory {
install_type: ExistingInstallType::OfficialLauncher,
path: from_home_dir("Documents/My Games/FINAL FANTASY XIV - A Realm Reborn"),
})
// TODO: Add Astra
}
&_ => {}
}
user_dirs
.into_iter()
.filter(|dir| is_valid_user_dir(&dir.path))
.collect()
}
fn from_home_dir(path: &'static str) -> String {
#[allow(deprecated)] // We still want std::env::home_dir
let mut new_path = home_dir().unwrap();
new_path.push(path);
new_path.into_os_string().into_string().unwrap()
}
fn is_valid_game_dir(path: &String) -> bool {
let mut d = PathBuf::from(path);
// Check for the dir itself
if fs::metadata(d.as_path()).is_err() {
return false;
}
// Check for "game"
d.push("game");
if fs::metadata(d.as_path()).is_err() {
return false;
}
// Check for "boot"
d.pop();
d.push("boot");
if fs::metadata(d.as_path()).is_err() {
return false;
}
true
}
fn is_valid_user_dir(path: &String) -> bool {
let mut d = PathBuf::from(path);
// Check for the dir itself
if fs::metadata(d.as_path()).is_err() {
return false;
}
// Check for "FFXIV.cfg"
d.push("FFXIV.cfg");
if fs::metadata(d.as_path()).is_err() {
return false;
}
true
}

148
src/installer.rs Normal file
View file

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
use std::ffi::{CStr, CString, NulError};
use std::fs;
use std::fs::File;
use std::io::Write;
use std::os::raw::c_char;
const FILES_TO_EXTRACT: [&str; 3] = ["data1.cab", "data1.hdr", "data2.cab"];
const BOOT_COMPONENT_FILES: [&str; 18] = [
"cef_license.txt",
"FFXIV.ico",
"ffxivboot.exe",
"ffxivboot.ver",
"ffxivboot64.exe",
"ffxivconfig.exe",
"ffxivconfig64.exe",
"ffxivlauncher.exe",
"ffxivlauncher64.exe",
"ffxivsysinfo.exe",
"ffxivsysinfo64.exe",
"ffxivupdater.exe",
"ffxivupdater64.exe",
"FFXIV_sysinfo.ico",
"icudt.dll",
"libcef.dll",
"license.txt",
"locales/reserved.txt",
];
const GAME_COMPONENT_FILES: [&str; 1] = ["ffxivgame.ver"];
#[repr(C)]
struct Unshield {
_private: [u8; 0],
}
unsafe extern "C" {
fn unshield_open(filename: *const c_char) -> *mut Unshield;
fn unshield_close(unshield: *mut Unshield);
fn unshield_set_log_level(level: i32);
fn unshield_file_count(unshield: *mut Unshield) -> i32;
fn unshield_file_name(unshield: *mut Unshield, index: i32) -> *const c_char;
fn unshield_file_save(unshield: *mut Unshield, index: i32, filename: *const c_char) -> bool;
}
pub enum InstallError {
IOFailure,
FFIFailure,
}
impl From<std::io::Error> for InstallError {
fn from(_: std::io::Error) -> Self {
InstallError::IOFailure
}
}
impl From<NulError> for InstallError {
fn from(_: NulError) -> Self {
InstallError::FFIFailure
}
}
/// Installs the game from the provided retail installer.
pub fn install_game(installer_path: &str, game_directory: &str) -> Result<(), InstallError> {
let installer_file = fs::read(installer_path).unwrap();
let mut last_position = 0;
let mut last_filename = "";
for filename in FILES_TO_EXTRACT {
let needle = format!("Disk1\\{}", filename);
let position = installer_file
.windows(needle.len())
.position(|window| window == needle.as_str().as_bytes());
if position == None {
break;
}
let position = position.unwrap();
if last_position != 0 {
let mut temp_dir = std::env::temp_dir();
temp_dir.push(last_filename);
let mut new_file = File::create(temp_dir).unwrap();
if last_filename == "data1.hdr" {
new_file.write_all(&installer_file[last_position + 30..position - 42])?;
} else {
new_file.write_all(&installer_file[last_position + 33..position - 42])?;
}
}
last_position = position;
last_filename = filename;
}
let mut temp_dir = std::env::temp_dir();
temp_dir.push(last_filename);
let mut new_file = File::create(temp_dir).unwrap();
new_file.write_all(&installer_file[last_position + 33..installer_file.len() - 42])?;
fs::create_dir_all(format!("{game_directory}/boot"))?;
fs::create_dir_all(format!("{game_directory}/game"))?;
// set unshield to shut up
unsafe { unshield_set_log_level(0) };
let mut temp_dir = std::env::temp_dir();
temp_dir.push("data1.cab");
let temp_dir_string = CString::new(temp_dir.to_str().unwrap())?;
let unshield = unsafe { unshield_open(temp_dir_string.as_ptr()) };
let file_count = unsafe { unshield_file_count(unshield) };
for i in 0..file_count {
let filename = unsafe { CStr::from_ptr(unshield_file_name(unshield, i)).to_string_lossy() };
for boot_name in BOOT_COMPONENT_FILES {
if boot_name == filename {
let save_filename = format!("{game_directory}/boot/{boot_name}");
let save_filename_c = CString::new(save_filename)?;
unsafe { unshield_file_save(unshield, i, save_filename_c.as_ptr()) };
}
}
for game_name in GAME_COMPONENT_FILES {
if game_name == filename {
let save_filename = format!("{game_directory}/game/{game_name}");
let save_filename_c = CString::new(save_filename)?;
unsafe { unshield_file_save(unshield, i, save_filename_c.as_ptr()) };
}
}
}
unsafe {
unshield_close(unshield);
}
Ok(())
}

9
src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
/// Find existing installation directories
pub mod existing_dirs;
/// Reading data from executables
pub mod execlookup;
/// Initializing a new retail game install from the official retail installer. No execution required!
#[cfg(feature = "game_install")]
pub mod installer;