From 863dccb5ea79b7f8b5a654f7a225be9b9220c057 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Thu, 29 Jul 2021 13:51:25 -0400 Subject: [PATCH] Modify extension bytecode to fix SimpleDateFormat cannot parse errors (#149) - Fixes sources like NHentai - Fixes mass testing - Cleans up a bit of the build.gradle.kts's - Fix Tsuki by setting a http.agent system property --- AndroidCompat/build.gradle.kts | 11 +- .../androidcompat/AndroidCompatInitializer.kt | 3 + .../replace/CalendarDelegate.java | 289 +++++++++++++++ .../replace/NumberFormatDelegate.java | 243 +++++++++++++ .../replace/SimpleDateFormat.java | 340 ++++++++++++++++++ .../replace/TimeZoneDelegate.java | 190 ++++++++++ build.gradle.kts | 2 +- server/build.gradle.kts | 6 +- .../tachidesk/anime/impl/util/PackageTools.kt | 3 + .../manga/impl/util/BytecodeEditor.kt | 218 +++++++++++ .../tachidesk/manga/impl/util/PackageTools.kt | 2 + .../tachidesk/manga/impl/util/storage/Io.kt | 24 ++ .../test/kotlin/suwayomi/TestExtensions.kt | 24 +- 13 files changed, 1338 insertions(+), 17 deletions(-) create mode 100644 AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/CalendarDelegate.java create mode 100644 AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/NumberFormatDelegate.java create mode 100644 AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/SimpleDateFormat.java create mode 100644 AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/TimeZoneDelegate.java create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/Io.kt diff --git a/AndroidCompat/build.gradle.kts b/AndroidCompat/build.gradle.kts index fd782bc6..9ca92f9a 100644 --- a/AndroidCompat/build.gradle.kts +++ b/AndroidCompat/build.gradle.kts @@ -25,13 +25,13 @@ dependencies { // compileOnly( fileTree(dir: new File(rootProject.rootDir, "libs/other"), include: "*.jar") // JSON - compileOnly( "com.google.code.gson:gson:2.8.6") + compileOnly("com.google.code.gson:gson:2.8.6") // Javassist - compileOnly( "org.javassist:javassist:3.27.0-GA") + compileOnly("org.javassist:javassist:3.27.0-GA") // XML - compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1") + compileOnly(group= "xmlpull", name= "xmlpull", version= "1.1.3.1") // Config API implementation(project(":AndroidCompat:Config")) @@ -40,7 +40,7 @@ dependencies { compileOnly("com.android.tools.build:apksig:4.2.0-alpha13") // AndroidX annotations - compileOnly( "androidx.annotation:annotation:1.2.0-alpha01") + compileOnly("androidx.annotation:annotation:1.2.0-alpha01") // substitute for duktape-android // 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead @@ -52,6 +52,9 @@ dependencies { val multiplatformSettingsVersion = "0.7.7" implementation("com.russhwolf:multiplatform-settings-jvm:$multiplatformSettingsVersion") implementation("com.russhwolf:multiplatform-settings-serialization-jvm:$multiplatformSettingsVersion") + + // Android version of SimpleDateFormat + implementation("com.ibm.icu:icu4j:69.1") } tasks { diff --git a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/AndroidCompatInitializer.kt b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/AndroidCompatInitializer.kt index 43572fec..f88d0a7e 100644 --- a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/AndroidCompatInitializer.kt +++ b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/AndroidCompatInitializer.kt @@ -26,5 +26,8 @@ class AndroidCompatInitializer { ApplicationInfoConfigModule.register(GlobalConfigManager.config), SystemConfigModule.register(GlobalConfigManager.config) ) + + // Set some properties extensions use + System.setProperty("http.agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") } } diff --git a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/CalendarDelegate.java b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/CalendarDelegate.java new file mode 100644 index 00000000..9b31774d --- /dev/null +++ b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/CalendarDelegate.java @@ -0,0 +1,289 @@ +package xyz.nulldev.androidcompat.replace; + +import com.ibm.icu.text.DateFormat; +import com.ibm.icu.util.TimeZone; +import com.ibm.icu.util.ULocale; + +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +public class CalendarDelegate extends Calendar { + private com.ibm.icu.util.Calendar delegate; + + public CalendarDelegate(com.ibm.icu.util.Calendar delegate) { + this.delegate = delegate; + } + + public static java.util.Calendar getInstance() { + return new CalendarDelegate(com.ibm.icu.util.Calendar.getInstance()); + } + + public static com.ibm.icu.util.Calendar getInstance(TimeZone zone) { + return com.ibm.icu.util.Calendar.getInstance(zone); + } + + public static java.util.Calendar getInstance(Locale aLocale) { + return new CalendarDelegate(com.ibm.icu.util.Calendar.getInstance(aLocale)); + } + + public static com.ibm.icu.util.Calendar getInstance(ULocale locale) { + return com.ibm.icu.util.Calendar.getInstance(locale); + } + + public static com.ibm.icu.util.Calendar getInstance(TimeZone zone, Locale aLocale) { + return com.ibm.icu.util.Calendar.getInstance(zone, aLocale); + } + + public static com.ibm.icu.util.Calendar getInstance(TimeZone zone, ULocale locale) { + return com.ibm.icu.util.Calendar.getInstance(zone, locale); + } + + public static Locale[] getAvailableLocales() { + return com.ibm.icu.util.Calendar.getAvailableLocales(); + } + + @Override + protected void computeTime() {} + + @Override + protected void computeFields() {} + + public static ULocale[] getAvailableULocales() { + return com.ibm.icu.util.Calendar.getAvailableULocales(); + } + + public static String[] getKeywordValuesForLocale(String key, ULocale locale, boolean commonlyUsed) { + return com.ibm.icu.util.Calendar.getKeywordValuesForLocale(key, locale, commonlyUsed); + } + + @Override + public long getTimeInMillis() { + return delegate.getTimeInMillis(); + } + + @Override + public void setTimeInMillis(long millis) { + delegate.setTimeInMillis(millis); + } + + @Deprecated + public int getRelatedYear() { + return delegate.getRelatedYear(); + } + + @Deprecated + public void setRelatedYear(int year) { + delegate.setRelatedYear(year); + } + + @Override + public boolean equals(Object obj) { + return delegate.equals(obj); + } + + public boolean isEquivalentTo(com.ibm.icu.util.Calendar other) { + return delegate.isEquivalentTo(other); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean before(Object when) { + return delegate.before(when); + } + + @Override + public boolean after(Object when) { + return delegate.after(when); + } + + @Override + public int getActualMaximum(int field) { + return delegate.getActualMaximum(field); + } + + @Override + public int getActualMinimum(int field) { + return delegate.getActualMinimum(field); + } + + @Override + public void roll(int field, int amount) { + delegate.roll(field, amount); + } + + @Override + public void add(int field, int amount) { + delegate.add(field, amount); + } + + @Override + public void roll(int field, boolean up) { + roll(field, up ? 1 : -1); + } + + public String getDisplayName(Locale loc) { + return delegate.getDisplayName(loc); + } + + public String getDisplayName(ULocale loc) { + return delegate.getDisplayName(loc); + } + + public int compareTo(com.ibm.icu.util.Calendar that) { + return delegate.compareTo(that); + } + + public DateFormat getDateTimeFormat(int dateStyle, int timeStyle, Locale loc) { + return delegate.getDateTimeFormat(dateStyle, timeStyle, loc); + } + + public DateFormat getDateTimeFormat(int dateStyle, int timeStyle, ULocale loc) { + return delegate.getDateTimeFormat(dateStyle, timeStyle, loc); + } + + @Deprecated + public static String getDateTimePattern(com.ibm.icu.util.Calendar cal, ULocale uLocale, int dateStyle) { + return com.ibm.icu.util.Calendar.getDateTimePattern(cal, uLocale, dateStyle); + } + + public int fieldDifference(Date when, int field) { + return delegate.fieldDifference(when, field); + } + + public void setTimeZone(TimeZone value) { + delegate.setTimeZone(value); + } + + @Override + public java.util.TimeZone getTimeZone() { + return new TimeZoneDelegate(delegate.getTimeZone()); + } + + @Override + public void setLenient(boolean lenient) { + delegate.setLenient(lenient); + } + + @Override + public boolean isLenient() { + return delegate.isLenient(); + } + + public void setRepeatedWallTimeOption(int option) { + delegate.setRepeatedWallTimeOption(option); + } + + public int getRepeatedWallTimeOption() { + return delegate.getRepeatedWallTimeOption(); + } + + public void setSkippedWallTimeOption(int option) { + delegate.setSkippedWallTimeOption(option); + } + + public int getSkippedWallTimeOption() { + return delegate.getSkippedWallTimeOption(); + } + + @Override + public void setFirstDayOfWeek(int value) { + delegate.setFirstDayOfWeek(value); + } + + @Override + public int getFirstDayOfWeek() { + return delegate.getFirstDayOfWeek(); + } + + @Override + public void setMinimalDaysInFirstWeek(int value) { + delegate.setMinimalDaysInFirstWeek(value); + } + + @Override + public int getMinimalDaysInFirstWeek() { + return delegate.getMinimalDaysInFirstWeek(); + } + + @Override + public int getMinimum(int field) { + return delegate.getMinimum(field); + } + + @Override + public int getMaximum(int field) { + return delegate.getMaximum(field); + } + + @Override + public int getGreatestMinimum(int field) { + return delegate.getGreatestMinimum(field); + } + + @Override + public int getLeastMaximum(int field) { + return delegate.getLeastMaximum(field); + } + + @Deprecated + public int getDayOfWeekType(int dayOfWeek) { + return delegate.getDayOfWeekType(dayOfWeek); + } + + @Deprecated + public int getWeekendTransition(int dayOfWeek) { + return delegate.getWeekendTransition(dayOfWeek); + } + + public boolean isWeekend(Date date) { + return delegate.isWeekend(date); + } + + public boolean isWeekend() { + return delegate.isWeekend(); + } + + @Override + public Object clone() { + return delegate.clone(); + } + + @Override + public String toString() { + return delegate.toString(); + } + + public static com.ibm.icu.util.Calendar.WeekData getWeekDataForRegion(String region) { + return com.ibm.icu.util.Calendar.getWeekDataForRegion(region); + } + + public com.ibm.icu.util.Calendar.WeekData getWeekData() { + return delegate.getWeekData(); + } + + public com.ibm.icu.util.Calendar setWeekData(com.ibm.icu.util.Calendar.WeekData wdata) { + return delegate.setWeekData(wdata); + } + + public int getFieldCount() { + return delegate.getFieldCount(); + } + + public String getType() { + return delegate.getType(); + } + + @Deprecated + public boolean haveDefaultCentury() { + return delegate.haveDefaultCentury(); + } + + public ULocale getLocale(ULocale.Type type) { + return delegate.getLocale(type); + } +} \ No newline at end of file diff --git a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/NumberFormatDelegate.java b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/NumberFormatDelegate.java new file mode 100644 index 00000000..0b209085 --- /dev/null +++ b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/NumberFormatDelegate.java @@ -0,0 +1,243 @@ +package xyz.nulldev.androidcompat.replace; + +import com.ibm.icu.text.DisplayContext; +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.util.Currency; +import com.ibm.icu.util.CurrencyAmount; +import com.ibm.icu.util.ULocale; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.AttributedCharacterIterator; +import java.text.FieldPosition; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Locale; + +public class NumberFormatDelegate extends java.text.NumberFormat { + private com.ibm.icu.text.NumberFormat delegate; + + public NumberFormatDelegate(com.ibm.icu.text.NumberFormat delegate) { + this.delegate = delegate; + } + + public StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(number, toAppendTo, pos); + } + + public String format(BigInteger number) { + return delegate.format(number); + } + + public String format(BigDecimal number) { + return delegate.format(number); + } + + public String format(com.ibm.icu.math.BigDecimal number) { + return delegate.format(number); + } + + public String format(CurrencyAmount currAmt) { + return delegate.format(currAmt); + } + + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(number, toAppendTo, pos); + } + + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(number, toAppendTo, pos); + } + + public StringBuffer format(BigInteger number, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(number, toAppendTo, pos); + } + + public StringBuffer format(BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(number, toAppendTo, pos); + } + + public StringBuffer format(com.ibm.icu.math.BigDecimal number, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(number, toAppendTo, pos); + } + + public StringBuffer format(CurrencyAmount currAmt, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(currAmt, toAppendTo, pos); + } + + public Number parse(String text, ParsePosition parsePosition) { + return delegate.parse(text, parsePosition); + } + + public Number parse(String text) throws ParseException { + return delegate.parse(text); + } + + public CurrencyAmount parseCurrency(CharSequence text, ParsePosition pos) { + return delegate.parseCurrency(text, pos); + } + + public boolean isParseIntegerOnly() { + return delegate.isParseIntegerOnly(); + } + + public void setParseIntegerOnly(boolean value) { + delegate.setParseIntegerOnly(value); + } + + public void setParseStrict(boolean value) { + delegate.setParseStrict(value); + } + + public boolean isParseStrict() { + return delegate.isParseStrict(); + } + + public void setContext(DisplayContext context) { + delegate.setContext(context); + } + + public DisplayContext getContext(DisplayContext.Type type) { + return delegate.getContext(type); + } + + public static java.text.NumberFormat getInstance(Locale inLocale) { + return new NumberFormatDelegate(NumberFormat.getInstance(inLocale)); + } + + public static NumberFormat getInstance(ULocale inLocale) { + return NumberFormat.getInstance(inLocale); + } + + public static NumberFormat getInstance(int style) { + return NumberFormat.getInstance(style); + } + + public static NumberFormat getInstance(Locale inLocale, int style) { + return NumberFormat.getInstance(inLocale, style); + } + + public static NumberFormat getNumberInstance(ULocale inLocale) { + return NumberFormat.getNumberInstance(inLocale); + } + + public static NumberFormat getIntegerInstance(ULocale inLocale) { + return NumberFormat.getIntegerInstance(inLocale); + } + + public static NumberFormat getCurrencyInstance(ULocale inLocale) { + return NumberFormat.getCurrencyInstance(inLocale); + } + + public static NumberFormat getPercentInstance(ULocale inLocale) { + return NumberFormat.getPercentInstance(inLocale); + } + + public static NumberFormat getScientificInstance(ULocale inLocale) { + return NumberFormat.getScientificInstance(inLocale); + } + + public static Locale[] getAvailableLocales() { + return NumberFormat.getAvailableLocales(); + } + + public static ULocale[] getAvailableULocales() { + return NumberFormat.getAvailableULocales(); + } + + public static Object registerFactory(NumberFormat.NumberFormatFactory factory) { + return NumberFormat.registerFactory(factory); + } + + public static boolean unregister(Object registryKey) { + return NumberFormat.unregister(registryKey); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return delegate.equals(obj); + } + + @Override + public Object clone() { + return delegate.clone(); + } + + public boolean isGroupingUsed() { + return delegate.isGroupingUsed(); + } + + public void setGroupingUsed(boolean newValue) { + delegate.setGroupingUsed(newValue); + } + + public int getMaximumIntegerDigits() { + return delegate.getMaximumIntegerDigits(); + } + + public void setMaximumIntegerDigits(int newValue) { + delegate.setMaximumIntegerDigits(newValue); + } + + public int getMinimumIntegerDigits() { + return delegate.getMinimumIntegerDigits(); + } + + public void setMinimumIntegerDigits(int newValue) { + delegate.setMinimumIntegerDigits(newValue); + } + + public int getMaximumFractionDigits() { + return delegate.getMaximumFractionDigits(); + } + + public void setMaximumFractionDigits(int newValue) { + delegate.setMaximumFractionDigits(newValue); + } + + public int getMinimumFractionDigits() { + return delegate.getMinimumFractionDigits(); + } + + public void setMinimumFractionDigits(int newValue) { + delegate.setMinimumFractionDigits(newValue); + } + + public void setCurrency(Currency theCurrency) { + delegate.setCurrency(theCurrency); + } + + public java.util.Currency getCurrency() { + return java.util.Currency.getInstance(delegate.getCurrency().getCurrencyCode()); + } + + public void setRoundingMode(int roundingMode) { + delegate.setRoundingMode(roundingMode); + } + + public static NumberFormat getInstance(ULocale desiredLocale, int choice) { + return NumberFormat.getInstance(desiredLocale, choice); + } + + @Deprecated + public static String getPatternForStyle(ULocale forLocale, int choice) { + return NumberFormat.getPatternForStyle(forLocale, choice); + } + + public ULocale getLocale(ULocale.Type type) { + return delegate.getLocale(type); + } + + public AttributedCharacterIterator formatToCharacterIterator(Object obj) { + return delegate.formatToCharacterIterator(obj); + } + + public Object parseObject(String source) throws ParseException { + return delegate.parseObject(source); + } +} \ No newline at end of file diff --git a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/SimpleDateFormat.java b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/SimpleDateFormat.java new file mode 100644 index 00000000..521cefd3 --- /dev/null +++ b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/SimpleDateFormat.java @@ -0,0 +1,340 @@ +package xyz.nulldev.androidcompat.replace; + +import com.ibm.icu.text.DateFormatSymbols; +import com.ibm.icu.text.DisplayContext; +import com.ibm.icu.text.NumberFormat; +import com.ibm.icu.text.TimeZoneFormat; +import com.ibm.icu.util.Calendar; +import com.ibm.icu.util.TimeZone; +import com.ibm.icu.util.ULocale; + +import java.text.*; +import java.util.Date; +import java.util.Locale; + +/** + * Overridden to switch to Android implementation + */ +public class SimpleDateFormat extends DateFormat { + private com.ibm.icu.text.SimpleDateFormat delegate; + + public SimpleDateFormat() { + delegate = new com.ibm.icu.text.SimpleDateFormat(); + } + + private SimpleDateFormat(com.ibm.icu.text.SimpleDateFormat delegate) { + this.delegate = delegate; + } + + public SimpleDateFormat(String pattern) { + delegate = new com.ibm.icu.text.SimpleDateFormat(pattern); + } + + public SimpleDateFormat(String pattern, Locale loc) { + delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, loc); + } + + public SimpleDateFormat(String pattern, ULocale loc) { + delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, loc); + } + + public SimpleDateFormat(String pattern, String override, ULocale loc) { + delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, override, loc); + } + + public SimpleDateFormat(String pattern, DateFormatSymbols formatData) { + delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, formatData); + } + + public SimpleDateFormat(String pattern, DateFormatSymbols formatData, ULocale loc) { + delegate = new com.ibm.icu.text.SimpleDateFormat(pattern, formatData, loc); + } + + @Deprecated + public static SimpleDateFormat getInstance(Calendar.FormatConfiguration formatConfig) { + return new SimpleDateFormat(com.ibm.icu.text.SimpleDateFormat.getInstance(formatConfig)); + } + + public void set2DigitYearStart(Date startDate) { + delegate.set2DigitYearStart(startDate); + } + + public Date get2DigitYearStart() { + return delegate.get2DigitYearStart(); + } + + public void setContext(DisplayContext context) { + delegate.setContext(context); + } + + public StringBuffer format(Calendar cal, StringBuffer toAppendTo, FieldPosition pos) { + return delegate.format(cal, toAppendTo, pos); + } + + public void setNumberFormat(NumberFormat newNumberFormat) { + delegate.setNumberFormat(newNumberFormat); + } + + public void parse(String text, Calendar cal, ParsePosition parsePos) { + delegate.parse(text, cal, parsePos); + } + + public String toPattern() { + return delegate.toPattern(); + } + + public String toLocalizedPattern() { + return delegate.toLocalizedPattern(); + } + + public void applyPattern(String pat) { + delegate.applyPattern(pat); + } + + public void applyLocalizedPattern(String pat) { + delegate.applyLocalizedPattern(pat); + } + + public DateFormatSymbols getDateFormatSymbols() { + return delegate.getDateFormatSymbols(); + } + + public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) { + delegate.setDateFormatSymbols(newFormatSymbols); + } + + public TimeZoneFormat getTimeZoneFormat() { + return delegate.getTimeZoneFormat(); + } + + public void setTimeZoneFormat(TimeZoneFormat tzfmt) { + delegate.setTimeZoneFormat(tzfmt); + } + + @Override + public Object clone() { + return delegate.clone(); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return delegate.equals(obj); + } + + @Override + public AttributedCharacterIterator formatToCharacterIterator(Object obj) { + return delegate.formatToCharacterIterator(obj); + } + + @Deprecated + public StringBuffer intervalFormatByAlgorithm(Calendar fromCalendar, Calendar toCalendar, StringBuffer appendTo, FieldPosition pos) throws IllegalArgumentException { + return delegate.intervalFormatByAlgorithm(fromCalendar, toCalendar, appendTo, pos); + } + + public void setNumberFormat(String fields, NumberFormat overrideNF) { + delegate.setNumberFormat(fields, overrideNF); + } + + public NumberFormat getNumberFormat(char field) { + return delegate.getNumberFormat(field); + } + + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + return delegate.format(date, toAppendTo, fieldPosition); + } + + @Override + public Date parse(String text) throws ParseException { + return delegate.parse(text); + } + + @Override + public Date parse(String text, ParsePosition pos) { + return delegate.parse(text, pos); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return delegate.parseObject(source, pos); + } + + public static com.ibm.icu.text.DateFormat getTimeInstance(int style, ULocale locale) { + return com.ibm.icu.text.DateFormat.getTimeInstance(style, locale); + } + + public static com.ibm.icu.text.DateFormat getDateInstance(int style, ULocale locale) { + return com.ibm.icu.text.DateFormat.getDateInstance(style, locale); + } + + public static com.ibm.icu.text.DateFormat getDateTimeInstance(int dateStyle, int timeStyle, ULocale locale) { + return com.ibm.icu.text.DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale); + } + + public static Locale[] getAvailableLocales() { + return com.ibm.icu.text.DateFormat.getAvailableLocales(); + } + + public static ULocale[] getAvailableULocales() { + return com.ibm.icu.text.DateFormat.getAvailableULocales(); + } + + @Override + public void setCalendar(java.util.Calendar newCalendar) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(newCalendar.getTimeZone().getID())); + cal.setTimeInMillis(newCalendar.getTimeInMillis()); + delegate.setCalendar(cal); + } + + @Override + public java.util.Calendar getCalendar() { + return new CalendarDelegate(delegate.getCalendar()); + } + + @Override + public java.text.NumberFormat getNumberFormat() { + return new NumberFormatDelegate(delegate.getNumberFormat()); + } + + @Override + public void setTimeZone(java.util.TimeZone zone) { + delegate.setTimeZone(TimeZone.getTimeZone(zone.getID())); + } + + @Override + public java.util.TimeZone getTimeZone() { + return new TimeZoneDelegate(delegate.getTimeZone()); + } + + @Override + public void setLenient(boolean lenient) { + delegate.setLenient(lenient); + } + + @Override + public boolean isLenient() { + return delegate.isLenient(); + } + + public void setCalendarLenient(boolean lenient) { + delegate.setCalendarLenient(lenient); + } + + public boolean isCalendarLenient() { + return delegate.isCalendarLenient(); + } + + public com.ibm.icu.text.DateFormat setBooleanAttribute(com.ibm.icu.text.DateFormat.BooleanAttribute key, boolean value) { + return delegate.setBooleanAttribute(key, value); + } + + public boolean getBooleanAttribute(com.ibm.icu.text.DateFormat.BooleanAttribute key) { + return delegate.getBooleanAttribute(key); + } + + public DisplayContext getContext(DisplayContext.Type type) { + return delegate.getContext(type); + } + + public static com.ibm.icu.text.DateFormat getDateInstance(Calendar cal, int dateStyle, Locale locale) { + return com.ibm.icu.text.DateFormat.getDateInstance(cal, dateStyle, locale); + } + + public static com.ibm.icu.text.DateFormat getDateInstance(Calendar cal, int dateStyle, ULocale locale) { + return com.ibm.icu.text.DateFormat.getDateInstance(cal, dateStyle, locale); + } + + public static com.ibm.icu.text.DateFormat getTimeInstance(Calendar cal, int timeStyle, Locale locale) { + return com.ibm.icu.text.DateFormat.getTimeInstance(cal, timeStyle, locale); + } + + public static com.ibm.icu.text.DateFormat getTimeInstance(Calendar cal, int timeStyle, ULocale locale) { + return com.ibm.icu.text.DateFormat.getTimeInstance(cal, timeStyle, locale); + } + + public static com.ibm.icu.text.DateFormat getDateTimeInstance(Calendar cal, int dateStyle, int timeStyle, Locale locale) { + return com.ibm.icu.text.DateFormat.getDateTimeInstance(cal, dateStyle, timeStyle, locale); + } + + public static com.ibm.icu.text.DateFormat getDateTimeInstance(Calendar cal, int dateStyle, int timeStyle, ULocale locale) { + return com.ibm.icu.text.DateFormat.getDateTimeInstance(cal, dateStyle, timeStyle, locale); + } + + public static com.ibm.icu.text.DateFormat getInstance(Calendar cal, Locale locale) { + return com.ibm.icu.text.DateFormat.getInstance(cal, locale); + } + + public static com.ibm.icu.text.DateFormat getInstance(Calendar cal, ULocale locale) { + return com.ibm.icu.text.DateFormat.getInstance(cal, locale); + } + + public static com.ibm.icu.text.DateFormat getInstance(Calendar cal) { + return com.ibm.icu.text.DateFormat.getInstance(cal); + } + + public static com.ibm.icu.text.DateFormat getDateInstance(Calendar cal, int dateStyle) { + return com.ibm.icu.text.DateFormat.getDateInstance(cal, dateStyle); + } + + public static com.ibm.icu.text.DateFormat getTimeInstance(Calendar cal, int timeStyle) { + return com.ibm.icu.text.DateFormat.getTimeInstance(cal, timeStyle); + } + + public static com.ibm.icu.text.DateFormat getDateTimeInstance(Calendar cal, int dateStyle, int timeStyle) { + return com.ibm.icu.text.DateFormat.getDateTimeInstance(cal, dateStyle, timeStyle); + } + + public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(String skeleton) { + return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(skeleton); + } + + public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(String skeleton, Locale locale) { + return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(skeleton, locale); + } + + public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(String skeleton, ULocale locale) { + return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(skeleton, locale); + } + + public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(Calendar cal, String skeleton, Locale locale) { + return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(cal, skeleton, locale); + } + + public static com.ibm.icu.text.DateFormat getInstanceForSkeleton(Calendar cal, String skeleton, ULocale locale) { + return com.ibm.icu.text.DateFormat.getInstanceForSkeleton(cal, skeleton, locale); + } + + public static com.ibm.icu.text.DateFormat getPatternInstance(String skeleton) { + return com.ibm.icu.text.DateFormat.getPatternInstance(skeleton); + } + + public static com.ibm.icu.text.DateFormat getPatternInstance(String skeleton, Locale locale) { + return com.ibm.icu.text.DateFormat.getPatternInstance(skeleton, locale); + } + + public static com.ibm.icu.text.DateFormat getPatternInstance(String skeleton, ULocale locale) { + return com.ibm.icu.text.DateFormat.getPatternInstance(skeleton, locale); + } + + public static com.ibm.icu.text.DateFormat getPatternInstance(Calendar cal, String skeleton, Locale locale) { + return com.ibm.icu.text.DateFormat.getPatternInstance(cal, skeleton, locale); + } + + public static com.ibm.icu.text.DateFormat getPatternInstance(Calendar cal, String skeleton, ULocale locale) { + return com.ibm.icu.text.DateFormat.getPatternInstance(cal, skeleton, locale); + } + + public ULocale getLocale(ULocale.Type type) { + return delegate.getLocale(type); + } + + @Override + public Object parseObject(String source) throws ParseException { + return delegate.parseObject(source); + } +} \ No newline at end of file diff --git a/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/TimeZoneDelegate.java b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/TimeZoneDelegate.java new file mode 100644 index 00000000..5b6af4eb --- /dev/null +++ b/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/replace/TimeZoneDelegate.java @@ -0,0 +1,190 @@ +package xyz.nulldev.androidcompat.replace; + +import com.ibm.icu.util.ULocale; + +import java.util.Date; +import java.util.Locale; +import java.util.Set; +import java.util.TimeZone; + +public class TimeZoneDelegate extends TimeZone { + private com.ibm.icu.util.TimeZone delegate; + + public TimeZoneDelegate(com.ibm.icu.util.TimeZone delegate) { + this.delegate = delegate; + } + + @Override + public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) { + return delegate.getOffset(era, year, month, day, dayOfWeek, milliseconds); + } + + @Override + public int getOffset(long date) { + return delegate.getOffset(date); + } + + public void getOffset(long date, boolean local, int[] offsets) { + delegate.getOffset(date, local, offsets); + } + + @Override + public void setRawOffset(int offsetMillis) { + delegate.setRawOffset(offsetMillis); + } + + @Override + public int getRawOffset() { + return delegate.getRawOffset(); + } + + @Override + public String getID() { + return delegate.getID(); + } + + @Override + public void setID(String ID) { + delegate.setID(ID); + } + + public String getDisplayName(ULocale locale) { + return delegate.getDisplayName(locale); + } + + @Override + public String getDisplayName(boolean daylight, int style, Locale locale) { + return delegate.getDisplayName(daylight, style, locale); + } + + public String getDisplayName(boolean daylight, int style, ULocale locale) { + return delegate.getDisplayName(daylight, style, locale); + } + + @Override + public int getDSTSavings() { + return delegate.getDSTSavings(); + } + + @Override + public boolean useDaylightTime() { + return delegate.useDaylightTime(); + } + + @Override + public boolean observesDaylightTime() { + return delegate.observesDaylightTime(); + } + + @Override + public boolean inDaylightTime(Date date) { + return delegate.inDaylightTime(date); + } + + public static TimeZone getTimeZone(String ID) { + return new TimeZoneDelegate(com.ibm.icu.util.TimeZone.getTimeZone(ID)); + } + + public static com.ibm.icu.util.TimeZone getFrozenTimeZone(String ID) { + return com.ibm.icu.util.TimeZone.getFrozenTimeZone(ID); + } + + public static com.ibm.icu.util.TimeZone getTimeZone(String ID, int type) { + return com.ibm.icu.util.TimeZone.getTimeZone(ID, type); + } + + public static void setDefaultTimeZoneType(int type) { + com.ibm.icu.util.TimeZone.setDefaultTimeZoneType(type); + } + + public static int getDefaultTimeZoneType() { + return com.ibm.icu.util.TimeZone.getDefaultTimeZoneType(); + } + + public static Set getAvailableIDs(com.ibm.icu.util.TimeZone.SystemTimeZoneType zoneType, String region, Integer rawOffset) { + return com.ibm.icu.util.TimeZone.getAvailableIDs(zoneType, region, rawOffset); + } + + public static String[] getAvailableIDs(int rawOffset) { + return com.ibm.icu.util.TimeZone.getAvailableIDs(rawOffset); + } + + public static String[] getAvailableIDs(String country) { + return com.ibm.icu.util.TimeZone.getAvailableIDs(country); + } + + public static String[] getAvailableIDs() { + return com.ibm.icu.util.TimeZone.getAvailableIDs(); + } + + public static int countEquivalentIDs(String id) { + return com.ibm.icu.util.TimeZone.countEquivalentIDs(id); + } + + public static String getEquivalentID(String id, int index) { + return com.ibm.icu.util.TimeZone.getEquivalentID(id, index); + } + + public static TimeZone getDefault() { + return new TimeZoneDelegate(com.ibm.icu.util.TimeZone.getDefault()); + } + + public static void setDefault(com.ibm.icu.util.TimeZone tz) { + com.ibm.icu.util.TimeZone.setDefault(tz); + } + + public boolean hasSameRules(com.ibm.icu.util.TimeZone other) { + return delegate.hasSameRules(other); + } + + @Override + public Object clone() { + return delegate.clone(); + } + + @Override + public boolean equals(Object obj) { + return delegate.equals(obj); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + public static String getTZDataVersion() { + return com.ibm.icu.util.TimeZone.getTZDataVersion(); + } + + public static String getCanonicalID(String id) { + return com.ibm.icu.util.TimeZone.getCanonicalID(id); + } + + public static String getCanonicalID(String id, boolean[] isSystemID) { + return com.ibm.icu.util.TimeZone.getCanonicalID(id, isSystemID); + } + + public static String getRegion(String id) { + return com.ibm.icu.util.TimeZone.getRegion(id); + } + + public static String getWindowsID(String id) { + return com.ibm.icu.util.TimeZone.getWindowsID(id); + } + + public static String getIDForWindowsID(String winid, String region) { + return com.ibm.icu.util.TimeZone.getIDForWindowsID(winid, region); + } + + public boolean isFrozen() { + return delegate.isFrozen(); + } + + public com.ibm.icu.util.TimeZone freeze() { + return delegate.freeze(); + } + + public com.ibm.icu.util.TimeZone cloneAsThawed() { + return delegate.cloneAsThawed(); + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index dbd45c95..be658d61 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,7 +43,7 @@ configure(projects) { // Kotlin implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) - testImplementation(kotlin("test")) + testImplementation(kotlin("test-junit5")) // coroutines val coroutinesVersion = "1.4.3" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 102124b5..7d23d805 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -58,6 +58,9 @@ dependencies { implementation("com.github.salomonbrys.kotson:kotson:2.5.0") + // asm for fixing SimpleDateFormat (must match Dex2Jar version) + implementation("org.ow2.asm:asm-debug-all:5.0.3") + // Source models and interfaces from Tachiyomi 1.x // using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi // implementation("tachiyomi.sourceapi:source-api:1.1") @@ -68,9 +71,6 @@ dependencies { // uncomment to test extensions directly // implementation(fileTree("lib/")) - - // Testing - testImplementation(kotlin("test-junit5")) } val MainClass = "suwayomi.tachidesk.MainKt" diff --git a/server/src/main/kotlin/suwayomi/tachidesk/anime/impl/util/PackageTools.kt b/server/src/main/kotlin/suwayomi/tachidesk/anime/impl/util/PackageTools.kt index 8bdf709f..d0db6658 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/anime/impl/util/PackageTools.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/anime/impl/util/PackageTools.kt @@ -22,6 +22,7 @@ import org.kodein.di.conf.global import org.kodein.di.instance import org.w3c.dom.Element import org.w3c.dom.Node +import suwayomi.tachidesk.manga.impl.util.BytecodeEditor import suwayomi.tachidesk.server.ApplicationDirs import xyz.nulldev.androidcompat.pm.InstalledPackage.Companion.toList import xyz.nulldev.androidcompat.pm.toPackageInfo @@ -80,6 +81,8 @@ object PackageTools { """.trimIndent() ) handler.dump(errorFile, emptyArray()) + } else { + BytecodeEditor.fixAndroidClasses(jarFilePath.toFile()) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt new file mode 100644 index 00000000..4a6d65f0 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/BytecodeEditor.kt @@ -0,0 +1,218 @@ +package suwayomi.tachidesk.manga.impl.util + +import mu.KotlinLogging +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.Handle +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode +import suwayomi.tachidesk.manga.impl.util.storage.use +import java.io.File +import java.io.IOException +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +object BytecodeEditor { + private val logger = KotlinLogging.logger {} + fun fixAndroidClasses(jarFile: File) { + val nodes = loadClasses(jarFile) + .mapValues { (className, classFileBuffer) -> + logger.trace { "Processing class $className" } + transform(classFileBuffer) + } + loadNonClasses(jarFile) + + saveAsJar(nodes, jarFile) + } + + private fun loadClasses(jar: File): Map { + return JarFile(jar).use { jarFile -> + jarFile.entries() + .asSequence() + .mapNotNull { + readJar(jarFile, it) + } + .toMap() + } + } + + private fun readJar(jar: JarFile, entry: JarEntry): Pair? { + return try { + jar.getInputStream(entry).use { stream -> + if (entry.name.endsWith(".class")) { + val bytes = stream.readBytes() + if (bytes.size < 4) { + // Invalid class size + return@use null + } + val cafebabe = String.format( + "%02X%02X%02X%02X", + bytes[0], + bytes[1], + bytes[2], + bytes[3] + ) + if (cafebabe.toLowerCase() != "cafebabe") { + // Corrupted class + return@use null + } + + getNode(bytes).name to bytes + } else null + } + } catch (e: IOException) { + logger.error(e) { "Error loading jar file" } + null + } + } + + private fun getNode(bytes: ByteArray): ClassNode { + val cr = ClassReader(bytes) + return ClassNode().also { cr.accept(it, ClassReader.EXPAND_FRAMES) } + } + + private const val simpleDateFormat = "java/text/SimpleDateFormat" + private const val replacementSimpleDateFormat = "xyz/nulldev/androidcompat/replace/SimpleDateFormat" + + private fun String?.replaceFormatFully() = if (this == simpleDateFormat) { + replacementSimpleDateFormat + } else this + private fun String?.replaceFormat() = this?.replace(simpleDateFormat, replacementSimpleDateFormat) + + private fun transform(classfileBuffer: ByteArray): ByteArray { + val cr = ClassReader(classfileBuffer) + val cw = ClassWriter(cr, 0) + cr.accept( + object : ClassVisitor(Opcodes.ASM5, cw) { + override fun visitField( + access: Int, + name: String?, + desc: String?, + signature: String?, + cst: Any? + ): FieldVisitor? { + logger.trace { "CLass Field" to "${desc.replaceFormat()}: ${cst?.let { it::class.java.simpleName }}: $cst" } + return super.visitField(access, name, desc.replaceFormat(), signature, cst) + } + + override fun visit( + version: Int, + access: Int, + name: String?, + signature: String?, + superName: String?, + interfaces: Array? + ) { + logger.trace { "Visiting $name: $signature: $superName" } + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor { + logger.trace { "Processing method $name: ${desc.replaceFormat()}: $signature" } + val mv: MethodVisitor? = super.visitMethod( + access, name, desc.replaceFormat(), signature, exceptions + ) + return object : MethodVisitor(Opcodes.ASM5, mv) { + override fun visitLdcInsn(cst: Any?) { + logger.trace { "Ldc" to "${cst?.let { "${it::class.java.simpleName}: $it" }}" } + super.visitLdcInsn(cst) + } + + override fun visitTypeInsn(opcode: Int, type: String?) { + logger.trace { + "Type" to "$opcode: ${type.replaceFormatFully()}" + } + super.visitTypeInsn( + opcode, + type.replaceFormatFully() + ) + } + + override fun visitMethodInsn( + opcode: Int, + owner: String?, + name: String?, + desc: String?, + itf: Boolean + ) { + logger.trace { + "Method" to "$opcode: ${owner.replaceFormatFully()}: $name: ${desc.replaceFormat()}" + } + super.visitMethodInsn( + opcode, + owner.replaceFormatFully(), + name, + desc.replaceFormat(), + itf + ) + } + + override fun visitFieldInsn( + opcode: Int, + owner: String?, + name: String?, + desc: String? + ) { + logger.trace { "Field" to "$opcode: $owner: $name: ${desc.replaceFormat()}" } + super.visitFieldInsn(opcode, owner, name, desc.replaceFormat()) + } + + override fun visitInvokeDynamicInsn( + name: String?, + desc: String?, + bsm: Handle?, + vararg bsmArgs: Any? + ) { + logger.trace { "InvokeDynamic" to "$name: $desc" } + super.visitInvokeDynamicInsn(name, desc, bsm, *bsmArgs) + } + } + } + }, + 0 + ) + return cw.toByteArray() + } + + private fun loadNonClasses(jarFile: File): Map { + val entries = mutableMapOf() + ZipInputStream(jarFile.inputStream()).use { stream -> + var nextEntry: ZipEntry? + while (stream.nextEntry.also { nextEntry = it } != null) { + nextEntry?.use(stream) { entry -> + if (!entry.name.endsWith(".class") && !entry.isDirectory) { + val bytes = stream.readBytes() + entries[entry.name] = bytes + } + } + } + } + return entries + } + + private fun saveAsJar(outBytes: Map, file: File) { + JarOutputStream(file.outputStream()).use { out -> + outBytes.forEach { (entry, value) -> + // Append extension to class entries + out.putNextEntry( + ZipEntry( + entry + if (entry.contains(".")) "" else ".class" + ) + ) + out.write(value) + out.closeEntry() + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt index c4ff1063..50207806 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/PackageTools.kt @@ -81,6 +81,8 @@ object PackageTools { """.trimIndent() ) handler.dump(errorFile, emptyArray()) + } else { + BytecodeEditor.fixAndroidClasses(jarFilePath.toFile()) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/Io.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/Io.kt new file mode 100644 index 00000000..45d5bc39 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/Io.kt @@ -0,0 +1,24 @@ +package suwayomi.tachidesk.manga.impl.util.storage + +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +fun ZipEntry.use(stream: ZipInputStream, block: (ZipEntry) -> Unit) { + var exception: Throwable? = null + try { + return block(this) + } catch (e: Throwable) { + exception = e + throw e + } finally { + if (exception == null) { + stream.closeEntry() + } else { + try { + stream.closeEntry() + } catch (closeException: Throwable) { + exception.addSuppressed(closeException) + } + } + } +} diff --git a/server/src/test/kotlin/suwayomi/TestExtensions.kt b/server/src/test/kotlin/suwayomi/TestExtensions.kt index cc484726..1dfcea67 100644 --- a/server/src/test/kotlin/suwayomi/TestExtensions.kt +++ b/server/src/test/kotlin/suwayomi/TestExtensions.kt @@ -31,6 +31,7 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.server.applicationSetup import java.io.File +import java.util.concurrent.atomic.AtomicInteger @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestExtensions { @@ -48,7 +49,7 @@ class TestExtensions { @BeforeAll fun setup() { val dataRoot = File("tmp/TestDesk").absolutePath - System.setProperty("suwayomi.tachidesk.rootDir", dataRoot) + System.setProperty("suwayomi.tachidesk.server.rootDir", dataRoot) applicationSetup() setLoggingEnabled(false) @@ -63,6 +64,7 @@ class TestExtensions { updateExtension(it.pkgName) } else -> { + uninstallExtension(it.pkgName) installExtension(it.pkgName) } } @@ -77,10 +79,11 @@ class TestExtensions { fun runTest() { runBlocking(Dispatchers.Default) { val semaphore = Semaphore(10) - sources.mapIndexed { index, source -> + val popularCount = AtomicInteger(1) + sources.map { source -> async { semaphore.withPermit { - logger.info { "$index - Now fetching popular manga from $source" } + logger.info { "${popularCount.getAndIncrement()} - Now fetching popular manga from $source" } try { mangaToFetch += source to ( source.fetchPopularManga(1) @@ -102,10 +105,11 @@ class TestExtensions { ) logger.info { "Now fetching manga info from ${mangaToFetch.size} sources" } - mangaToFetch.mapIndexed { index, (source, manga) -> + val mangaCount = AtomicInteger(1) + mangaToFetch.map { (source, manga) -> async { semaphore.withPermit { - logger.info { "$index - Now fetching manga from $source" } + logger.info { "${mangaCount.getAndIncrement()} - Now fetching manga from $source" } try { manga.copyFrom(source.fetchMangaDetails(manga).awaitSingleRepeat()) manga.initialized = true @@ -127,10 +131,11 @@ class TestExtensions { ) logger.info { "Now fetching manga chapters from ${mangaToFetch.size} sources" } - mangaToFetch.filter { it.second.initialized }.mapIndexed { index, (source, manga) -> + val chapterCount = AtomicInteger(1) + mangaToFetch.filter { it.second.initialized }.map { (source, manga) -> async { semaphore.withPermit { - logger.info { "$index - Now fetching manga chapters from $source" } + logger.info { "${chapterCount.getAndIncrement()} - Now fetching manga chapters from $source" } try { chaptersToFetch += Triple( source, @@ -160,10 +165,11 @@ class TestExtensions { } ) - chaptersToFetch.mapIndexed { index, (source, manga, chapter) -> + val pageListCount = AtomicInteger(1) + chaptersToFetch.map { (source, manga, chapter) -> async { semaphore.withPermit { - logger.info { "$index - Now fetching page list from $source" } + logger.info { "${pageListCount.getAndIncrement()} - Now fetching page list from $source" } try { source.fetchPageList(chapter).awaitSingleRepeat() } catch (e: Exception) {