diff options
| -rw-r--r-- | Cargo.lock | 190 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | src/launcher.rs | 150 | ||||
| -rw-r--r-- | src/launcher/assets.rs | 5 | ||||
| -rw-r--r-- | src/launcher/constants.rs | 8 |
5 files changed, 321 insertions, 35 deletions
@@ -90,7 +90,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -124,6 +124,15 @@ dependencies = [ ] [[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] name = "arrayref" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -435,6 +444,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] name = "calloop" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -445,7 +475,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -543,7 +573,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" dependencies = [ - "thiserror", + "thiserror 1.0.69", "x11rb", ] @@ -744,6 +774,21 @@ dependencies = [ ] [[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -839,6 +884,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" [[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -848,6 +899,17 @@ dependencies = [ ] [[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] name = "detect-desktop-environment" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1395,7 +1457,7 @@ checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.69", "winapi", "windows 0.52.0", ] @@ -1494,7 +1556,7 @@ dependencies = [ "com", "libc", "libloading 0.8.6", - "thiserror", + "thiserror 1.0.69", "widestring", "winapi", ] @@ -1669,7 +1731,7 @@ dependencies = [ "iced_renderer", "iced_widget", "iced_winit", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1688,7 +1750,7 @@ dependencies = [ "palette", "rustc-hash 2.1.0", "smol_str", - "thiserror", + "thiserror 1.0.69", "web-time", ] @@ -1736,7 +1798,7 @@ dependencies = [ "once_cell", "raw-window-handle", "rustc-hash 2.1.0", - "thiserror", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -1750,7 +1812,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1763,7 +1825,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1798,7 +1860,7 @@ dependencies = [ "log", "once_cell", "rustc-hash 2.1.0", - "thiserror", + "thiserror 1.0.69", "wgpu", ] @@ -1813,7 +1875,7 @@ dependencies = [ "num-traits", "once_cell", "rustc-hash 2.1.0", - "thiserror", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -1828,7 +1890,7 @@ dependencies = [ "iced_runtime", "log", "rustc-hash 2.1.0", - "thiserror", + "thiserror 1.0.69", "tracing", "wasm-bindgen-futures", "web-sys", @@ -2018,7 +2080,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -2153,6 +2215,12 @@ dependencies = [ ] [[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2165,6 +2233,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" [[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2255,7 +2333,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] @@ -2288,7 +2366,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2399,6 +2477,7 @@ dependencies = [ "chrono", "const_format", "futures", + "lazy_static", "log", "regex", "reqwest", @@ -2407,6 +2486,8 @@ dependencies = [ "sha1_smol", "sysinfo", "tokio", + "tokio-stream", + "zip", ] [[package]] @@ -3131,7 +3212,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3573,7 +3654,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.69", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -3816,7 +3897,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -3831,6 +3921,17 @@ dependencies = [ ] [[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.93", +] + +[[package]] name = "time" version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3975,6 +4076,17 @@ dependencies = [ ] [[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] name = "tokio-util" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4498,7 +4610,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "web-sys", "wgpu-hal", "wgpu-types", @@ -4542,7 +4654,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "web-sys", "wgpu-types", @@ -4608,7 +4720,7 @@ dependencies = [ "clipboard_wayland", "clipboard_x11", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5251,6 +5363,40 @@ dependencies = [ ] [[package]] +name = "zip" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" +dependencies = [ + "arbitrary", + "bzip2", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "indexmap", + "lzma-rs", + "memchr", + "thiserror 2.0.11", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] name = "zvariant" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7,6 +7,7 @@ edition = "2021" chrono = { version = "0.4.39", default-features = false, features = ["std", "alloc", "clock", "now", "serde"] } const_format = "0.2.34" futures = "0.3.31" +lazy_static = "1.5.0" log = "0.4.22" regex = "1.11.1" reqwest = { version = "0.12.12", features = ["json", "stream"] } @@ -15,6 +16,8 @@ serde_json = "1.0.133" sha1_smol = { version = "1.0.1", features = ["alloc", "std", "serde"] } sysinfo = { version = "0.33.1", features = ["system", "multithread"] } tokio = { version = "1.42.0", features = ["fs", "io-util", "sync", "rt"] } +tokio-stream = { version = "0.1.17", features = ["fs"] } +zip = { version = "2.2.2", default-features = false, features = ["bzip2", "deflate", "deflate64", "lzma", "xz"] } [workspace] members = [ "ozone-cli", "ozone-ui" ] diff --git a/src/launcher.rs b/src/launcher.rs index 25cdcc4..f712e69 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -7,16 +7,21 @@ mod rules; mod assets; use std::borrow::Cow; +use std::cmp::min; use std::collections::HashMap; use std::env::consts::{ARCH, OS}; use std::error::Error; use std::ffi::OsStr; use std::fmt::{Display, Formatter}; +use std::fs::FileType; use std::io::ErrorKind; use std::io::ErrorKind::AlreadyExists; use std::path::{Component, Path, PathBuf}; +use std::process; +use std::time::{SystemTime, UNIX_EPOCH}; +use chrono::NaiveDateTime; use const_format::formatcp; -use futures::TryStreamExt; +use futures::{stream, FutureExt, StreamExt, TryStreamExt}; use log::{debug, info, warn}; use serde::{Deserialize, Serialize}; use sha1_smol::Sha1; @@ -24,10 +29,12 @@ use sysinfo::System; use tokio::{fs, io}; use tokio::fs::File; use tokio::io::AsyncWriteExt; +use tokio_stream::wrappers::ReadDirStream; +use zip::ZipArchive; use download::{MultiDownloader, VerifiedDownload}; use rules::{CompatCheck, IncompatibleError}; use version::{VersionList, VersionResolveError, VersionResult}; -use crate::version::{Logging, Library, OSRestriction, OperatingSystem, DownloadType, DownloadInfo}; +use crate::version::{Logging, Library, OSRestriction, OperatingSystem, DownloadType, DownloadInfo, LibraryExtractRule}; pub use profile::{Instance, Profile}; use crate::launcher::assets::{AssetError, AssetRepository}; @@ -125,7 +132,8 @@ struct SystemInfo { } struct LibraryRepository { - home: PathBuf + home: PathBuf, + natives: PathBuf } pub struct Launcher { @@ -154,6 +162,7 @@ pub enum LaunchError { LibraryDirError(PathBuf, io::Error), LibraryVerifyError(FileVerifyError), LibraryDownloadError, + LibraryExtractZipError(PathBuf, zip::result::ZipError), // ensure file errors MissingURL, @@ -180,6 +189,7 @@ impl Display for LaunchError { LaunchError::LibraryDirError(path, e) => write!(f, "failed to create library directory {}: {}", path.display(), e), LaunchError::LibraryVerifyError(e) => write!(f, "failed to verify library: {}", e), LaunchError::LibraryDownloadError => f.write_str("library download failed (see above logs for details)"), // TODO: booo this sucks + LaunchError::LibraryExtractZipError(path, e) => write!(f, "library extract zip error ({}): {e}", path.display()), LaunchError::MissingURL => f.write_str("cannot download required file, URL is missing"), LaunchError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), LaunchError::Offline => f.write_str("cannot download file in offline mode"), @@ -201,6 +211,7 @@ impl Error for LaunchError { LaunchError::IncompatibleVersion(e) => Some(e), LaunchError::LibraryDirError(_, e) => Some(e), LaunchError::LibraryVerifyError(e) => Some(e), + LaunchError::LibraryExtractZipError(_, e) => Some(e), LaunchError::IO { error: e, .. } => Some(e), LaunchError::Download { error: e, .. } => Some(e), LaunchError::Integrity(e) => Some(e), @@ -245,12 +256,24 @@ impl Launcher { system_info: SystemInfo::new(), libraries: LibraryRepository { home: home.join("libraries"), + natives: home.join("natives") }, assets: AssetRepository::new(online, &assets_path).await?, home }) } + fn choose_lib_classifier<'lib>(&self, lib: &'lib Library) -> Option<&'lib str> { + lib.natives.as_ref().map_or(None, |n| n.get(&self.system_info.os)).map(|s| s.as_str()) + } + + fn create_extract_job<'lib>(&self, lib: &'lib Library) -> Option<LibraryExtractJob<'lib>> { + Some(LibraryExtractJob { + source: LibraryRepository::get_artifact_path(lib.name.as_str(), self.choose_lib_classifier(lib))?, + rule: lib.extract.as_ref() + }) + } + async fn ensure_file(&self, path: &Path, dlinfo: &DownloadInfo) -> Result<(), LaunchError> { // verify the file match util::verify_file(path, dlinfo.size, dlinfo.sha1).await { @@ -387,7 +410,7 @@ impl Launcher { * - (done) download them * - (done) (if offline mode, explode) * - if virtual or resource-mapped, copy (or perhaps hardlink? that would be cool) - * - the actual client jar + * - (done) the actual client jar * - (done) check integriddy and download if needed * - (done) (explode if offline mode) * - launch the game @@ -418,7 +441,7 @@ impl Launcher { } libs.push(lib); - if let Some(dl) = self.libraries.create_download(lib, self.system_info.os) { + if let Some(dl) = self.libraries.create_download(lib, self.choose_lib_classifier(lib)) { dl.make_dirs().await.map_err(|e| LaunchError::LibraryDirError(dl.get_path().to_path_buf(), e))?; downloads.push(dl); } @@ -477,27 +500,45 @@ impl Launcher { client_jar_path = None; } + // clean up old natives + let nnatives = self.libraries.clean_old_natives().await?; + info!("Cleaned up {} old natives directories.", nnatives); + + // extract natives + //todo!() Ok(()) } } -#[derive(Debug, Clone)] +#[derive(Debug)] enum LibraryError { InvalidName(String), - IOError(ErrorKind) + IO { what: &'static str, error: io::Error } } impl Display for LibraryError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { LibraryError::InvalidName(name) => write!(f, "invalid name: {name}"), - LibraryError::IOError(e) => write!(f, "io error reading library: {e}"), + LibraryError::IO { what, error } => write!(f, "library i/o error ({what}): {error}"), + } + } +} + +impl Error for LibraryError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + LibraryError::IO { error, .. } => Some(error), + _ => None } } } -impl Error for LibraryError {} +struct LibraryExtractJob<'lib> { + source: PathBuf, + rule: Option<&'lib LibraryExtractRule> +} const ARCH_BITS: &'static str = formatcp!("{}", usize::BITS); @@ -542,9 +583,7 @@ impl LibraryRepository { Some(p) } - fn create_download(&self, lib: &Library, os: OperatingSystem) -> Option<VerifiedDownload> { - let classifier = lib.natives.as_ref().map_or(None, |n| n.get(&os)).map(|s| s.as_str()); - + fn create_download(&self, lib: &Library, classifier: Option<&str>) -> Option<VerifiedDownload> { if lib.url.is_some() || lib.downloads.is_none() { // TODO: derive download URL in this situation? warn!("BUG: Deprecated case for library {}: url present or downloads missing. The launcher does not support out-of-line checksums at this time. Not downloading this library.", lib.name); @@ -557,6 +596,93 @@ impl LibraryRepository { Some(VerifiedDownload::new(dlinfo.url.as_ref()?, path.as_path(), dlinfo.size, dlinfo.sha1)) } + + async fn clean_old_natives(&self) -> Result<usize, LaunchError> { + info!("Cleaning up old natives folders..."); + + let boot_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - min(System::uptime(), 7u64*24*60*60); + + let readdir = match fs::read_dir(&self.natives).await { + Ok(readdir) => readdir, + Err(e) if e.kind() == ErrorKind::NotFound => return Ok(0), + Err(e) => return Err(LaunchError::IO { what: "reading natives directory", error: e }) + }; + + ReadDirStream::new(readdir) + .map(|entry| Ok(async move { + let entry = entry.map_err(|e| LaunchError::IO { what: "reading natives entry", error: e })?; + let ftype = entry.file_type().await.map_err(|e| LaunchError::IO { what: "'stat'ing natives entry", error: e })?; + + if !ftype.is_dir() { return Ok(false); } + + let Some(ftime) = entry.file_name().to_str() + .map_or(None, |s| constants::NATIVES_DIR_PATTERN.captures(s)) + .map_or(None, |c| c.get(1)) + .map_or(None, |cap| cap.as_str().parse::<u64>().ok()) else { + return Ok(false); + }; + + if ftime < boot_time { + let path = entry.path(); + info!("Deleting old natives directory {}", path.display()); + + /*fs::remove_dir_all(&path).await.map_err(|e| LaunchError::IO { + what: "reading natives entry", + error: e + })?;*/ + + return Ok(true); + } + + Ok(false) + })) + .try_buffer_unordered(8) + .try_fold(0usize, |accum, res| async move { + match res { + true => Ok(accum + 1), + _ => Ok(accum) + } + }).await + } + + fn extract_natives_for<'lib>(natives: &Path, job: LibraryExtractJob<'lib>) -> Result<usize, LaunchError> { + /*let file = File::open(&job.source).await.map_err(|e| LaunchError::IO { + what: "extracting library natives (open)", + error: e + })?; + + let z = ZipArchive::new(file);*/ + + todo!() + } + + async fn extract_natives<'lib>(&self, libs: impl IntoIterator<Item = LibraryExtractJob<'lib>>) -> Result<PathBuf, LaunchError> { + fs::create_dir_all(&self.natives).await.map_err(|e| LaunchError::IO { + what: "creating natives directory", + error: e + })?; + + let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let natives_dir = self.natives.join(format!("{}{}-{}", constants::NATIVES_PREFIX, time, process::id())); + + // create_dir_all suppresses "AlreadyExists", but this is a fatal error here. + fs::create_dir(&natives_dir).await.map_err(|e| LaunchError::IO { + what: "creating natives directory", + error: e + })?; + + let natives_dir_ref = natives_dir.as_path(); + + stream::iter(libs) + .map(|lib| Ok(async move { + // TODO: Self::extract_natives_for(natives_dir_ref, lib) + todo!() as Result<usize, _> + })) + .try_buffer_unordered(8) + .try_fold(0usize, |accum, num| async move {Ok(accum + num)}) + .await.inspect(|num| debug!("Extracted {} native entries to {}", num, natives_dir_ref.display())) + .map(|_| natives_dir) + } } impl SystemInfo { diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs index 7ad368e..e732877 100644 --- a/src/launcher/assets.rs +++ b/src/launcher/assets.rs @@ -168,7 +168,10 @@ impl AssetRepository { if let Some(expect) = index.sha1 {
let actual = Sha1::from(&idx_text).digest();
- return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual }));
+
+ if actual != expect {
+ return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual }));
+ }
}
debug!("Saving downloaded asset index to {}", path.display());
diff --git a/src/launcher/constants.rs b/src/launcher/constants.rs index 83611c9..a0ef036 100644 --- a/src/launcher/constants.rs +++ b/src/launcher/constants.rs @@ -1,4 +1,6 @@ use const_format::formatcp; +use lazy_static::lazy_static; +use regex::Regex; const PKG_NAME: &str = env!("CARGO_PKG_NAME"); const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -8,3 +10,9 @@ pub const USER_AGENT: &str = formatcp!("{PKG_NAME}/{PKG_VERSION} (in {CRATE_NAME pub const URL_VERSION_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; pub const URL_RESOURCE_BASE: &str = "https://resources.download.minecraft.net/"; pub const URL_JRE_MANIFEST: &str = "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"; + +pub const NATIVES_PREFIX: &str = "natives-"; + +lazy_static! { + pub static ref NATIVES_DIR_PATTERN: Regex = Regex::new("^natives-(\\d+)").unwrap(); +}
\ No newline at end of file |
