66
77use std:: {
88 cmp:: Ordering , convert:: TryFrom , fmt:: Display , path:: PathBuf , str:: FromStr ,
9- time:: Duration ,
9+ sync :: LazyLock , time:: Duration ,
1010} ;
1111
1212use backon:: { ExponentialBuilder , Retryable } ;
@@ -17,6 +17,18 @@ use serde::{Deserialize, Serialize};
1717pub mod minecraft;
1818/// Models and methods for fetching metadata for Minecraft mod loaders
1919pub mod modded;
20+ /// Custom version comparison for Minecraft versions
21+ pub mod version;
22+
23+ /// HTTP client configuration constants
24+ /// TCP keepalive interval for persistent connections
25+ const TCP_KEEPALIVE_SECS : u64 = 10 ;
26+ /// Overall request timeout including reading response
27+ const REQUEST_TIMEOUT_SECS : u64 = 120 ;
28+ /// Connection establishment timeout
29+ const CONNECT_TIMEOUT_SECS : u64 = 30 ;
30+ /// Maximum idle connections per host in the pool
31+ const MAX_IDLE_CONNECTIONS_PER_HOST : usize = 10 ;
2032
2133/// Your branding, used for the user agent and similar
2234#[ derive( Debug ) ]
@@ -30,6 +42,30 @@ pub struct Branding {
3042/// The branding of your application
3143pub static BRANDING : OnceCell < Branding > = OnceCell :: new ( ) ;
3244
45+ /// Global HTTP client with connection pooling and TCP keepalive
46+ ///
47+ /// # Panics
48+ /// Panics if the HTTP client fails to initialize. This is intentional as
49+ /// the application cannot function without a working HTTP client (e.g., if
50+ /// TLS initialization fails, which is extremely rare on modern systems).
51+ static HTTP_CLIENT : LazyLock < reqwest:: Client > = LazyLock :: new ( || {
52+ let mut headers = reqwest:: header:: HeaderMap :: new ( ) ;
53+ if let Ok ( header) = reqwest:: header:: HeaderValue :: from_str (
54+ & BRANDING . get_or_init ( Branding :: default) . header_value ,
55+ ) {
56+ headers. insert ( reqwest:: header:: USER_AGENT , header) ;
57+ }
58+
59+ reqwest:: Client :: builder ( )
60+ . tcp_keepalive ( Some ( Duration :: from_secs ( TCP_KEEPALIVE_SECS ) ) )
61+ . timeout ( Duration :: from_secs ( REQUEST_TIMEOUT_SECS ) )
62+ . connect_timeout ( Duration :: from_secs ( CONNECT_TIMEOUT_SECS ) )
63+ . default_headers ( headers)
64+ . pool_max_idle_per_host ( MAX_IDLE_CONNECTIONS_PER_HOST )
65+ . build ( )
66+ . expect ( "Failed to create HTTP client" )
67+ } ) ;
68+
3369impl Branding {
3470 /// Creates a new branding instance
3571 pub fn new ( name : String , email : String ) -> Branding {
@@ -100,7 +136,6 @@ pub enum Error {
100136 MirrorsFailed ( String ) ,
101137}
102138
103- #[ cfg_attr( feature = "bincode" , derive( Encode , Decode ) ) ]
104139#[ derive( PartialEq , Eq , PartialOrd , Ord , Hash , Debug , Clone , Default ) ]
105140/// A specifier string for Gradle
106141pub struct GradleSpecifier {
@@ -166,12 +201,10 @@ impl GradleSpecifier {
166201
167202 /// Returns if specifier belongs to a lwjgl library
168203 pub fn is_lwjgl ( & self ) -> bool {
169- vec ! [
170- "org.lwjgl" ,
204+ [ "org.lwjgl" ,
171205 "org.lwjgl.lwjgl" ,
172206 "net.java.jinput" ,
173- "net.java.jutils" ,
174- ]
207+ "net.java.jutils" ]
175208 . contains ( & self . package . as_str ( ) )
176209 }
177210
@@ -186,7 +219,7 @@ impl GradleSpecifier {
186219 "{}:{}:{}" ,
187220 self . package,
188221 self . artifact,
189- self . identifier. clone ( ) . unwrap_or( "" . to_string ( ) )
222+ self . identifier. as_deref ( ) . unwrap_or( "" )
190223 )
191224 }
192225
@@ -195,17 +228,12 @@ impl GradleSpecifier {
195228 /// Returns Ordering::Greater if self is greater than other
196229 /// Returns Ordering::Less if self is less than other
197230 pub fn compare_versions ( & self , other : & Self ) -> Result < Ordering , Error > {
198- let x = lenient_semver:: parse ( self . version . as_str ( ) ) ;
199- let y = lenient_semver:: parse ( other. version . as_str ( ) ) ;
231+ let x = lenient_semver:: parse ( self . version . as_str ( ) )
232+ . map_err ( |_| Error :: ParseError ( "Unable to parse version" . to_string ( ) ) ) ?;
233+ let y = lenient_semver:: parse ( other. version . as_str ( ) )
234+ . map_err ( |_| Error :: ParseError ( "Unable to parse version" . to_string ( ) ) ) ?;
200235
201- if x. is_err ( ) || y. is_err ( ) {
202- return Err ( Error :: ParseError (
203- "Unable to parse version" . to_string ( ) ,
204- ) ) ;
205- }
206-
207- // safe to unwrap because we already checked for errors
208- Ok ( x. unwrap ( ) . cmp ( & y. unwrap ( ) ) )
236+ Ok ( x. cmp ( & y) )
209237 }
210238}
211239
@@ -358,32 +386,16 @@ pub async fn download_file_mirrors(
358386 }
359387 }
360388
361- return Err ( Error :: MirrorsFailed ( "No mirrors succeeded!" . to_string ( ) ) ) ;
389+ Err ( Error :: MirrorsFailed ( "No mirrors succeeded!" . to_string ( ) ) )
362390}
363391
364392/// Downloads a file with retry and checksum functionality
365393pub async fn download_file (
366394 url : & str ,
367395 sha1 : Option < & str > ,
368396) -> Result < bytes:: Bytes , Error > {
369- let mut headers = reqwest:: header:: HeaderMap :: new ( ) ;
370- if let Ok ( header) = reqwest:: header:: HeaderValue :: from_str (
371- & BRANDING . get_or_init ( Branding :: default) . header_value ,
372- ) {
373- headers. insert ( reqwest:: header:: USER_AGENT , header) ;
374- }
375- let client = reqwest:: Client :: builder ( )
376- . tcp_keepalive ( Some ( std:: time:: Duration :: from_secs ( 10 ) ) )
377- . timeout ( std:: time:: Duration :: from_secs ( 15 ) )
378- . default_headers ( headers)
379- . build ( )
380- . map_err ( |err| Error :: FetchError {
381- inner : err,
382- item : url. to_string ( ) ,
383- } ) ?;
384-
385397 ( || async {
386- let result = client . get ( url) . send ( ) . await ;
398+ let result = HTTP_CLIENT . get ( url) . send ( ) . await ;
387399
388400 match result {
389401 Ok ( x) => {
0 commit comments