11package me .itzg .helpers .fabric ;
22
3+ import java .io .BufferedReader ;
34import java .io .IOException ;
5+ import java .nio .file .Files ;
46import java .nio .file .Path ;
57import java .time .Duration ;
68import java .util .List ;
9+ import java .util .Properties ;
710import java .util .function .Predicate ;
811import lombok .Setter ;
12+ import lombok .extern .slf4j .Slf4j ;
913import me .itzg .helpers .errors .GenericException ;
14+ import me .itzg .helpers .errors .InvalidContentException ;
15+ import me .itzg .helpers .files .IoStreams ;
1016import me .itzg .helpers .http .FileDownloadStatusHandler ;
1117import me .itzg .helpers .http .SharedFetch ;
1218import me .itzg .helpers .http .UriBuilder ;
1319import org .jetbrains .annotations .NotNull ;
1420import org .jetbrains .annotations .Nullable ;
1521import reactor .core .publisher .Mono ;
22+ import reactor .core .scheduler .Schedulers ;
1623import reactor .util .retry .Retry ;
1724
25+ @ Slf4j
1826public class FabricMetaClient {
1927
2028 private final SharedFetch sharedFetch ;
@@ -32,7 +40,7 @@ public class FabricMetaClient {
3240 private Duration retryMinBackoff = Duration .ofMillis (500 );
3341
3442 @ Setter
35- private int downloadRetryMaxAttempts = 5 ;
43+ private int downloadRetryMaxAttempts = 10 ;
3644 @ Setter
3745 private Duration downloadRetryMinBackoff = Duration .ofMillis (500 );
3846
@@ -146,9 +154,55 @@ public Mono<Path> downloadLauncher(
146154 .handleStatus (statusHandler )
147155 .assemble ()
148156 .retryWhen (Retry .backoff (downloadRetryMaxAttempts , downloadRetryMinBackoff ).filter (IOException .class ::isInstance ))
157+ .flatMap (this ::validateLauncherJar )
158+ .retryWhen (Retry .backoff (downloadRetryMaxAttempts , downloadRetryMinBackoff ).filter (InvalidContentException .class ::isInstance ))
149159 .checkpoint ("downloadLauncher" );
150160 }
151161
162+ private Mono <Path > validateLauncherJar (Path path ) {
163+ return Mono .fromCallable (() -> {
164+ log .debug ("Validating Fabric launcher file {}" , path );
165+
166+ if (!path .toFile ().isFile ()) {
167+ throw new InvalidContentException ("Downloaded launcher jar is not a file" );
168+ }
169+ try {
170+ final Properties installProperties = IoStreams .readFileFromZip (path , "install.properties" , in -> {
171+ Properties p = new Properties ();
172+ p .load (in );
173+ return p ;
174+ }
175+ );
176+ if (installProperties == null ) {
177+ debugDownloadedContent (path );
178+ throw new InvalidContentException ("Downloaded launcher jar does not contain an install.properties" );
179+ }
180+ if (!installProperties .containsKey ("game-version" )) {
181+ debugDownloadedContent (path );
182+ throw new InvalidContentException ("Downloaded launcher jar does not contain a valid install.properties" );
183+ }
184+ } catch (IOException e ) {
185+ debugDownloadedContent (path );
186+ throw new InvalidContentException ("Downloaded launcher jar could not be read as a jar/zip" , e );
187+ }
188+
189+ return path ;
190+ })
191+ .subscribeOn (Schedulers .boundedElastic ());
192+ }
193+
194+ private static void debugDownloadedContent (Path path ) {
195+ if (log .isDebugEnabled ()) {
196+ try (BufferedReader reader = Files .newBufferedReader (path )) {
197+ final char [] buf = new char [100 ];
198+ final int amount = reader .read (buf );
199+ log .debug ("Downloaded launcher jar content starts with: {}" , new String (buf , 0 , amount ));
200+ } catch (IOException e ) {
201+ throw new GenericException ("Failed to read downloaded launcher jar for debugging" , e );
202+ }
203+ }
204+ }
205+
152206 @ NotNull
153207 private static Mono <String > findFirst (List <VersionEntry > versionEntries , Predicate <VersionEntry > condition
154208 ) {
0 commit comments