2022-05-06 13:39:00 +02:00
package com.falsepattern.lib.dependencies ;
2022-05-06 23:53:11 +02:00
import com.falsepattern.lib.StableAPI ;
2022-06-10 00:09:13 +02:00
import com.falsepattern.lib.internal.* ;
2022-06-08 16:01:06 +02:00
import java.io.BufferedOutputStream ;
import java.io.File ;
import java.io.IOException ;
import java.io.InputStream ;
import java.net.URL ;
2022-06-08 16:06:35 +02:00
import java.nio.file.Files ;
2022-07-24 11:13:15 +02:00
import java.nio.file.Paths ;
2022-07-24 11:23:34 +02:00
import java.security.MessageDigest ;
2022-06-08 16:01:06 +02:00
import java.util.HashMap ;
import java.util.HashSet ;
import java.util.Map ;
import java.util.Set ;
2022-07-23 00:00:17 +02:00
import java.util.concurrent.atomic.AtomicBoolean ;
2022-06-09 22:18:49 +02:00
2022-06-15 18:39:15 +02:00
import com.falsepattern.lib.util.FileUtil ;
2022-06-10 00:09:13 +02:00
import lombok.* ;
2022-07-24 11:34:55 +02:00
import org.apache.logging.log4j.LogManager ;
import org.apache.logging.log4j.Logger ;
2022-05-06 13:39:00 +02:00
import net.minecraft.launchwrapper.LaunchClassLoader ;
2022-05-06 23:53:11 +02:00
@StableAPI ( since = "0.6.0" )
2022-05-06 13:39:00 +02:00
public class DependencyLoader {
2022-07-24 11:23:34 +02:00
/**
* Dependency checksum formats in a decreasing order of quality.
*/
private static final String [] CHECKSUM_TYPES = new String [] { "sha512" , "sha256" , "sha1" , "md5" };
2022-05-06 13:39:00 +02:00
private static final Map < String , Version > loadedLibraries = new HashMap <> ();
private static final Map < String , String > loadedLibraryMods = new HashMap <> ();
private static final Set < String > mavenRepositories = new HashSet <> ();
2022-07-24 11:34:55 +02:00
private static final Logger log = LogManager . getLogger ( Tags . MODNAME + " Dependency Downloader" );
2022-05-06 13:39:00 +02:00
public static void addMavenRepo ( String url ) {
mavenRepositories . add ( url );
}
2022-06-08 18:57:08 +02:00
@Builder
2022-06-10 00:09:13 +02:00
public static void loadLibrary ( @NonNull String loadingModId ,
@NonNull String groupId ,
@NonNull String artifactId ,
2022-06-08 16:01:06 +02:00
@NonNull Version minVersion ,
Version maxVersion ,
@NonNull Version preferredVersion ,
String regularSuffix ,
String devSuffix ) {
2022-05-06 13:39:00 +02:00
val suffix = FalsePatternLib . isDeveloperEnvironment () ? devSuffix : regularSuffix ;
2022-06-10 00:09:13 +02:00
val artifactLogName = String . format ( "%s:%s:%s%s" , groupId , artifactId , preferredVersion , suffix != null ? "-" + suffix : "" );
2022-07-24 11:34:55 +02:00
log . info ( "Adding library {}, requested by mod {}" , artifactLogName , loadingModId );
2022-05-06 13:39:00 +02:00
var artifact = groupId + ":" + artifactId + ":" + suffix ;
if ( loadedLibraries . containsKey ( artifact )) {
val currentVer = loadedLibraries . get ( artifact );
2022-06-08 16:01:06 +02:00
if ( currentVer . equals ( preferredVersion )) {
return ;
}
2022-05-06 13:39:00 +02:00
val rangeString = "(minimum: " + minVersion + ( maxVersion == null ? "" : ", maximum: " + maxVersion ) + ")" ;
if ( minVersion . compareTo ( currentVer ) > 0 || ( maxVersion != null && maxVersion . compareTo ( currentVer ) < 0 )) {
for ( int i = 0 ; i < 16 ; i ++ ) {
2022-07-24 11:34:55 +02:00
log . fatal ( "ALERT VVVVVVVVVVVV ALERT" );
2022-05-06 13:39:00 +02:00
}
2022-07-24 11:34:55 +02:00
log . fatal ( "Library {}:{}{} already loaded with version {}, " +
"but a version in the range {} was requested! Thing may go horribly wrong! " +
"Requested by mod: {}, previously loaded by mod: {}" ,
groupId ,
artifactId ,
suffix != null ? ":" + suffix : "" ,
currentVer ,
rangeString ,
loadingModId ,
loadedLibraryMods . get ( artifact ));
2022-05-06 13:39:00 +02:00
for ( int i = 0 ; i < 16 ; i ++ ) {
2022-07-24 11:34:55 +02:00
log . fatal ( "ALERT ^^^^^^^^^^^^ ALERT" );
2022-05-06 13:39:00 +02:00
}
} else {
2022-07-24 11:34:55 +02:00
log . info ( "Attempted loading of library {}:{}{} with preferred version {}, " +
"but version {} was already loaded, which matched the range {}. This is not an " +
"error. " + "Requested by mod: {}, previously loaded by mod: {}" ,
groupId ,
artifactId ,
suffix != null ? ":" + suffix : "" ,
preferredVersion ,
currentVer ,
rangeString ,
loadingModId ,
loadedLibraryMods . get ( artifact ));
2022-05-06 13:39:00 +02:00
}
return ;
}
2022-07-24 11:13:15 +02:00
String homeDir = System . getProperty ( "minecraft.sharedDataDir" );
if ( homeDir == null ) {
homeDir = System . getenv ( "MINECRAFT_SHARED_DATA_DIR" );
if ( homeDir == null ) {
homeDir = FileUtil . getMinecraftHome (). getAbsolutePath ();
}
}
val modsDir = Paths . get ( homeDir , "mods" ). toFile ();
2022-06-08 16:01:06 +02:00
val mavenJarName =
String . format ( "%s-%s%s.jar" , artifactId , preferredVersion , ( suffix != null ) ? ( "-" + suffix ) : "" );
2022-05-06 13:39:00 +02:00
val jarName = groupId + "-" + mavenJarName ;
val libDir = new File ( modsDir , "falsepattern" );
if ( ! libDir . exists ()) {
2022-06-08 16:06:35 +02:00
if ( ! libDir . mkdirs ()) {
2022-07-24 11:34:55 +02:00
log . fatal ( "Failed to create directory {}" , libDir );
2022-06-08 16:06:35 +02:00
throw new RuntimeException ( "Failed to create directory " + libDir );
}
2022-05-06 13:39:00 +02:00
}
val file = new File ( libDir , jarName );
if ( file . exists ()) {
try {
addToClasspath ( file );
loadedLibraries . put ( artifact , preferredVersion );
2022-07-24 11:47:04 +02:00
log . debug ( "Library {} successfully loaded from disk!" , artifactLogName );
2022-05-06 13:39:00 +02:00
return ;
} catch ( RuntimeException e ) {
2022-07-24 11:34:55 +02:00
log . warn ( "Failed to load library {} from file! Re-downloading..." , artifactLogName );
2022-07-24 11:35:07 +02:00
checkedDelete ( file );
2022-05-06 13:39:00 +02:00
}
}
2022-06-09 22:18:49 +02:00
if ( ! LibraryConfig . ENABLE_LIBRARY_DOWNLOADS ) {
val errorMessage = "Failed to load library " + groupId + ":" + artifactId + ":" + preferredVersion +
2022-06-10 00:09:13 +02:00
(( suffix != null ) ? ":" + suffix : "" ) + ": " + Tags . MODNAME +
" library downloading has been disabled in the config, and the library is not present " +
"on disk! Requested by mod: " + loadingModId ;
2022-07-24 11:34:55 +02:00
log . fatal ( errorMessage );
2022-06-09 22:18:49 +02:00
throw new IllegalStateException ( errorMessage );
}
2022-07-24 11:23:34 +02:00
for ( var repo : mavenRepositories ) {
try {
if ( ! repo . endsWith ( "/" )) {
repo = repo + "/" ;
}
val url = String . format ( "%s%s/%s/%s/%s" , repo , groupId . replace ( '.' , '/' ), artifactId ,
preferredVersion , mavenJarName );
String finalRepo = repo ;
boolean retry = true ;
int retryCount = 0 ;
redownload :
while ( retry ) {
retry = false ;
retryCount ++ ;
if ( retryCount >= 3 ) {
break ;
}
val success = new AtomicBoolean ( false );
2022-07-24 11:34:55 +02:00
Internet . connect ( new URL ( url ),
2022-07-24 11:47:04 +02:00
( ex ) -> log . debug ( "Artifact {} could not be downloaded from repo {}: {}" , artifactLogName ,
2022-07-24 11:34:55 +02:00
finalRepo , ex . getMessage ()),
( input ) -> {
2022-07-24 11:47:04 +02:00
log . debug ( "Downloading {} from {}" , artifactLogName , finalRepo );
2022-07-24 11:23:34 +02:00
download ( input , file );
2022-07-24 11:47:04 +02:00
log . debug ( "Downloaded {} from {}" , artifactLogName , finalRepo );
2022-07-24 11:23:34 +02:00
success . set ( true );
});
if ( success . get ()) {
2022-07-24 11:47:04 +02:00
log . debug ( "Validating checksum for {}" , artifactLogName );
2022-07-24 11:23:34 +02:00
boolean hadChecksum = false ;
for ( val checksumType : CHECKSUM_TYPES ) {
val checksumURL = url + "." + checksumType ;
val checksumFile = new File ( libDir , jarName + "." + checksumType );
2022-07-24 11:47:04 +02:00
log . debug ( "Attempting to get {} checksum..." , checksumType );
2022-07-24 11:23:34 +02:00
success . set ( false );
2022-07-24 11:34:55 +02:00
Internet . connect ( new URL ( checksumURL ),
2022-07-24 11:47:04 +02:00
( ex ) -> log . debug ( "Could not get {} checksum for {}: {}" , checksumType ,
2022-07-24 11:34:55 +02:00
artifactLogName , ex . getMessage ()),
( input ) -> {
2022-07-24 11:47:04 +02:00
log . debug ( "Downloading {} checksum for {}" , checksumType , artifactLogName );
2022-07-24 11:23:34 +02:00
download ( input , checksumFile );
2022-07-24 11:47:04 +02:00
log . debug ( "Downloaded {} checksum for {}" , checksumType , artifactLogName );
2022-07-24 11:23:34 +02:00
success . set ( true );
});
if ( success . get ()) {
val fileHash = hash ( checksumType , file );
val referenceHash = new String ( Files . readAllBytes ( checksumFile . toPath ()));
if ( ! fileHash . equals ( referenceHash )) {
2022-07-24 11:34:55 +02:00
log . error ( "Failed {} checksum validation for {}. Retrying download..." ,
checksumType , artifactLogName );
2022-07-24 11:35:07 +02:00
checkedDelete ( file );
checkedDelete ( checksumFile );
2022-07-24 11:23:34 +02:00
retry = true ;
continue redownload ;
}
2022-07-24 11:47:04 +02:00
log . debug ( "Successfully validated {} checksum for {}" , checksumType ,
2022-07-24 11:34:55 +02:00
artifactLogName );
2022-07-24 11:23:34 +02:00
hadChecksum = true ;
2022-07-24 11:29:07 +02:00
break ;
2022-07-24 11:23:34 +02:00
}
}
if ( ! hadChecksum ) {
2022-07-24 11:34:55 +02:00
log . warn ( "The library {} had no checksum available on the repository.\n" +
"There's a chance it might have gotten corrupted during download,\n" +
"but we're loading it anyways." , artifactLogName );
2022-07-24 11:23:34 +02:00
}
loadedLibraries . put ( artifact , preferredVersion );
loadedLibraryMods . put ( artifact , loadingModId );
addToClasspath ( file );
return ;
}
}
} catch ( IOException ignored ) {
2022-06-08 16:01:06 +02:00
}
2022-05-06 13:39:00 +02:00
}
2022-07-24 11:23:34 +02:00
val errorMessage = "Failed to download library " + groupId + ":" + artifactId + ":" + preferredVersion + (( suffix != null ) ? ":" + suffix : "" ) + " from any repository! Requested by mod: " +
2022-06-08 16:01:06 +02:00
loadingModId ;
2022-07-24 11:34:55 +02:00
log . fatal ( errorMessage );
2022-05-06 13:39:00 +02:00
throw new IllegalStateException ( errorMessage );
}
2022-07-24 11:23:34 +02:00
private static String bytesToHex ( byte [] hash ) {
StringBuilder hexString = new StringBuilder ( 2 * hash . length );
for ( byte b : hash ) {
String hex = Integer . toHexString ( 0xff & b );
if ( hex . length () == 1 ) {
hexString . append ( '0' );
}
hexString . append ( hex );
}
return hexString . toString ();
}
@SneakyThrows
private static String digest ( String algo , byte [] data ) {
return bytesToHex ( MessageDigest . getInstance ( algo ). digest ( data ));
}
@SneakyThrows
private static String hash ( String algo , File file ) {
byte [] data = Files . readAllBytes ( file . toPath ());
switch ( algo ) {
case "md5" :
algo = "MD5" ;
break ;
case "sha1" :
algo = "SHA-1" ;
break ;
case "sha256" :
algo = "SHA-256" ;
break ;
case "sha512" :
algo = "SHA-512" ;
break ;
}
return digest ( algo , data );
}
2022-07-24 11:35:07 +02:00
private static void checkedDelete ( File file ) {
if ( ! file . delete ()) {
log . fatal ( "Failed to delete file {}" , file );
throw new RuntimeException ( "Failed to delete file " + file );
}
}
2022-05-06 13:39:00 +02:00
private static void addToClasspath ( File file ) {
try {
val cl = ( LaunchClassLoader ) DependencyLoader . class . getClassLoader ();
cl . addURL ( file . toURI (). toURL ());
2022-07-24 11:47:04 +02:00
log . debug ( "Injected file {} into classpath!" , file . getPath ());
2022-05-06 13:39:00 +02:00
} catch ( Exception e ) {
throw new RuntimeException ( "Failed to add library to classpath: " + file . getAbsolutePath (), e );
}
}
2022-06-10 00:09:13 +02:00
@SneakyThrows
private static void download ( InputStream is , File target ) {
2022-06-08 16:01:06 +02:00
if ( target . exists ()) {
return ;
}
2022-06-10 00:09:13 +02:00
Internet . transferAndClose ( is , new BufferedOutputStream ( Files . newOutputStream ( target . toPath ())));
2022-05-06 13:39:00 +02:00
}
}