From 643b4d43d323814d84067caa915e2b46866db2c1 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Sun, 8 Nov 2026 21:16:30 +0100 Subject: [PATCH 1/2] Bump the vendored engine to 7a923f40, engine master The engine factored library code out of its existing files, which adds two things the Android build needs. src/htsbase.c joins the explicit source list, and -DHTS_INTERNAL_BYTECODE shows libhttrack the engine-only half of the headers. Without the macro every engine file fails to compile, because the internal prototypes are now hidden behind it. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- app/src/main/jni/Android.mk | 3 +++ app/src/main/jni/httrack | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/jni/Android.mk b/app/src/main/jni/Android.mk index 8bd1a6d..e49247f 100755 --- a/app/src/main/jni/Android.mk +++ b/app/src/main/jni/Android.mk @@ -74,6 +74,7 @@ LOCAL_SRC_FILES := httrack/src/htscore.c httrack/src/htsparse.c \ httrack/src/htstools.c httrack/src/htswizard.c httrack/src/htsalias.c \ httrack/src/htsthread.c httrack/src/htsindex.c httrack/src/htsbauth.c \ httrack/src/htsbundle.c \ + httrack/src/htsbase.c \ httrack/src/htsmd5.c httrack/src/htszlib.c httrack/src/htswrap.c \ httrack/src/htsconcat.c httrack/src/htsmodules.c \ httrack/src/htscharset.c httrack/src/punycode.c \ @@ -113,6 +114,8 @@ LOCAL_CFLAGS += -O3 -g3 -funwind-tables -fPIC \ -Wno-nested-externs \ -D_REENTRANT -DPIC -DANDROID -D_ANDROID -DHAVE_CONFIG_H -DINET6 -DHTS_INTERNAL_BUILD \ -DLIBHTTRACK_EXPORTS -DZLIB_CONST -DHTS_INTHASH_USES_MD5 -DLIBICONV_PLUG +# HTS_INTERNAL_BYTECODE shows the engine-only half of the headers to the library. +LOCAL_CFLAGS += -DHTS_INTERNAL_BYTECODE # The app never asks the engine for a self-test. The sources are gone from # LOCAL_SRC_FILES above too, because htsselftest_int.h #errors under this macro. LOCAL_CFLAGS += -DHTS_NO_SELFTEST diff --git a/app/src/main/jni/httrack b/app/src/main/jni/httrack index 483d5d4..7a923f4 160000 --- a/app/src/main/jni/httrack +++ b/app/src/main/jni/httrack @@ -1 +1 @@ -Subproject commit 483d5d47a6243cf57d1495e473a09a09519f3dea +Subproject commit 7a923f4047df1bec18e2ce1b4403c7038d055021 From dc010852203af86ef508594b8ad05da2057537ea Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Sun, 8 Nov 2026 21:27:25 +0100 Subject: [PATCH 2/2] Clamp a crawl rate the new engine refuses instead of sending it The engine now refuses a -%c rate between zero and 0.000001, and one above a float, where it used to cast whatever it read (engine #2081). The rate field takes any decimal and no layout bounds it, so a user-typed 0.0000001 would abort the crawl before it starts. A magnitude clamps to the bound it passed, per the rule a profile value follows everywhere else here. The added test reads both bounds back out of the engine source, so a moved floor fails here instead of shipping a rate the engine panics on. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- .../com/httrack/android/OptionValues.java | 24 ++++++++++ .../com/httrack/android/OptionsMapper.java | 5 ++- .../android/NumericOptionValueTest.java | 45 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/httrack/android/OptionValues.java b/app/src/main/java/com/httrack/android/OptionValues.java index d56d525..76bfee1 100644 --- a/app/src/main/java/com/httrack/android/OptionValues.java +++ b/app/src/main/java/com/httrack/android/OptionValues.java @@ -21,6 +21,7 @@ package com.httrack.android; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; @@ -103,6 +104,29 @@ public static boolean isAtMost(final String value, final long max) { } } + /** Lowest non-zero rate the engine takes for -%c, and the highest. */ + private static final String MIN_RATE = "0.000001"; + + private static final String MAX_RATE = new BigDecimal(Float.MAX_VALUE) + .toPlainString(); + + /** + * The rate brought inside the range the engine accepts. The engine panics on + * a rate between zero and 0.000001 or above a float, and a zero means no + * limit, so it stays. + * + * @param value + * A value isDecimal accepted + * @return the value, or the nearest rate the engine takes + */ + public static String clampRate(final String value) { + final double rate = Double.parseDouble(value); + if (rate > 0 && rate < Double.parseDouble(MIN_RATE)) { + return MIN_RATE; + } + return rate > Float.MAX_VALUE ? MAX_RATE : value; + } + /** * True if the value is at most max bytes of UTF-8. An engine buffer is a byte * count, so a limit measured in characters lets a non-ASCII value through. diff --git a/app/src/main/java/com/httrack/android/OptionsMapper.java b/app/src/main/java/com/httrack/android/OptionsMapper.java index 4d6e5c5..002df20 100755 --- a/app/src/main/java/com/httrack/android/OptionsMapper.java +++ b/app/src/main/java/com/httrack/android/OptionsMapper.java @@ -1736,9 +1736,12 @@ public void emit(final List commandline, final String value) { * as a URL, which turns -r1.5 into an exclusion filter. */ /* Over the cap the engine refuses the command line or misreads the * value, and no layout bounds these fields. */ + /* A rate the engine refuses is clamped, because dropping it would leave + * the crawl with no rate limit at all. */ if (fraction ? OptionValues.isDecimal(value) : OptionValues.isAtMost( value, max)) { - commandline.add("-" + option + value); + commandline.add( + "-" + option + (fraction ? OptionValues.clampRate(value) : value)); } else { refused(commandline, value); } diff --git a/app/src/test/java/com/httrack/android/NumericOptionValueTest.java b/app/src/test/java/com/httrack/android/NumericOptionValueTest.java index 63598f4..580b04c 100644 --- a/app/src/test/java/com/httrack/android/NumericOptionValueTest.java +++ b/app/src/test/java/com/httrack/android/NumericOptionValueTest.java @@ -141,6 +141,51 @@ public void aSizeTheEngineWouldPanicOnNeverReachesIt() { } } + /** The float option's own bounds, which the pinned engine enforces. */ + private static final String MIN_RATE = "0.000001"; + + private static final String MAX_RATE = + "340282346638528859811704183484516925440"; + + /* The engine panics on a rate between zero and the floor, and on one above a + float, where it used to cast whatever it read. */ + @Test + public void aRateTheEngineWouldPanicOnIsClampedToTheBound() { + final OptionMapper mapper = new SimpleOption(FLOAT_OPTION, true); + for (final String value : new String[] { "0.0000001", "0.0000000001", + "0.00000099" }) { + assertEquals("-" + FLOAT_OPTION + MIN_RATE, emit(mapper, value).get(0)); + } + assertEquals("-" + FLOAT_OPTION + MAX_RATE, + emit(mapper, "99999999999999999999999999999999999999999").get(0)); + } + + /** A zero means no limit and the engine still takes it. */ + @Test + public void aRateTheEngineTakesIsSentUnchanged() { + final OptionMapper mapper = new SimpleOption(FLOAT_OPTION, true); + for (final String value : new String[] { "0", "0.0", MIN_RATE, "0.5", "5.0", + "100" }) { + assertEquals("-" + FLOAT_OPTION + value, emit(mapper, value).get(0)); + } + } + + /* The bounds live in the engine, so read them back: a moved floor must fail + here rather than ship a rate the engine panics on. */ + @Test + public void theClampedBoundsAreTheOnesTheEngineChecks() throws IOException { + final String main = TestSources + .read(TestSources.engineFile("src/htscoremain.c")); + final String gate = TestSources.balancedBlock(main, + TestSources.indexOf(main, "case 'c': {")); + assertTrue("the engine no longer bounds -" + FLOAT_OPTION + " at 1e-6", + gate.contains("v >= 1e-6f")); + assertTrue("the engine no longer bounds -" + FLOAT_OPTION + " at FLT_MAX", + gate.contains("v <= FLT_MAX")); + assertEquals(1e-6, Double.parseDouble(MIN_RATE), 0.0); + assertEquals(Float.MAX_VALUE, Double.parseDouble(MAX_RATE), 0.0); + } + @Test public void isPositiveRejectsWhatIsDigitsAccepts() { assertTrue(OptionValues.isDigits("0"));