From eff1fcef382cdc62f226173772e734000a7448f0 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 17 Aug 2026 21:59:59 +0200 Subject: [PATCH 1/3] Write the pause key under the name the other front ends read Android wrote Pause for the -%G random inter-file pause; WinHTTrack and WebHTTrack both write PauseFiles, so a profile carrying one was silently missing the setting for the other two. Take the majority spelling and read the old one through the existing rename shim. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- .../main/java/com/httrack/android/OptionsMapper.java | 6 +++--- .../com/httrack/android/WinProfileParityTest.java | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/httrack/android/OptionsMapper.java b/app/src/main/java/com/httrack/android/OptionsMapper.java index 8abc1392..ceb67d03 100755 --- a/app/src/main/java/com/httrack/android/OptionsMapper.java +++ b/app/src/main/java/com/httrack/android/OptionsMapper.java @@ -88,7 +88,7 @@ public class OptionsMapper { new Pair(R.id.editRetries, "Retry"), new Pair(R.id.editMinTransferRate, "RateOut"), new Pair(R.id.checkRemoveHostIfSlow, "RemoveRateout"), - new Pair(R.id.editPause, "Pause"), + new Pair(R.id.editPause, "PauseFiles"), /* Links */ new Pair(R.id.checkDetectAllLinks, "ParseAll"), @@ -348,7 +348,7 @@ public class OptionsMapper { new Pair("Proxy", proxyHandler.getAddressMapper()), new Pair("Port", proxyHandler.getPortMapper()), new Pair("CookiesFile", new ArgumentOption("-%K")), - new Pair("Pause", new ArgumentOption("-%G")), + new Pair("PauseFiles", new ArgumentOption("-%G")), new Pair("StripQuery", new ArgumentOption("-%g")), new Pair("HostAlias", new RuleListOption( "--host-alias")), @@ -420,7 +420,7 @@ public static class ProfileFormat { // Spellings earlier builds wrote; accepted on read, never written back. private static final String LEGACY_NAMES[][] = { { "ProxyProtocol", "ProxyType" }, { "KeepWwwPrefix", "KeepWww" }, - { "KeepDoubleSlashes", "KeepSlashes" } }; + { "KeepDoubleSlashes", "KeepSlashes" }, { "Pause", "PauseFiles" } }; // WinHTTrack packs both name-mangling boxes in Dos; Iso9660 is ours alone. static final String DOS_KEY = "Dos"; diff --git a/app/src/test/java/com/httrack/android/WinProfileParityTest.java b/app/src/test/java/com/httrack/android/WinProfileParityTest.java index fc2ddd3b..1125f7be 100644 --- a/app/src/test/java/com/httrack/android/WinProfileParityTest.java +++ b/app/src/test/java/com/httrack/android/WinProfileParityTest.java @@ -89,11 +89,12 @@ public void winHttrackProfileOpensWithBothBoxes() { public void olderProfileKeepsEveryRenamedSetting() { final Map values = ProfileFormat.resolve(file("Dos", "1", "Iso9660", "1", "ProxyProtocol", "1", "KeepWwwPrefix", "1", - "KeepDoubleSlashes", "1")); + "KeepDoubleSlashes", "1", "Pause", "2:5")); assertArrayEquals(new String[] { "1", "1" }, boxes(values)); assertEquals("1", values.get("ProxyType")); assertEquals("1", values.get("KeepWww")); assertEquals("1", values.get("KeepSlashes")); + assertEquals("2:5", values.get("PauseFiles")); assertFalse(values.containsKey("ProxyProtocol")); } @@ -159,6 +160,8 @@ public void renamedKeysMapBothWays() { assertEquals("KeepWww", ProfileFormat.canonicalName("KeepWwwPrefix")); assertEquals("KeepSlashes", ProfileFormat.canonicalName("KeepDoubleSlashes")); + assertEquals("PauseFiles", ProfileFormat.canonicalName("Pause")); + assertEquals("Pause", ProfileFormat.legacyName("PauseFiles")); assertEquals("Near", ProfileFormat.canonicalName("Near")); assertNull(ProfileFormat.legacyName("Near")); } @@ -184,9 +187,9 @@ private static List serializerKeys() throws IOException { public void keysUseTheWinHttrackSpelling() throws IOException { final List keys = serializerKeys(); assertTrue(keys.containsAll(Arrays.asList("ProxyType", "KeepWww", - "KeepSlashes"))); + "KeepSlashes", "PauseFiles"))); for (final String legacy : new String[] { "ProxyProtocol", - "KeepWwwPrefix", "KeepDoubleSlashes" }) { + "KeepWwwPrefix", "KeepDoubleSlashes", "Pause" }) { assertFalse(legacy + " still written", keys.contains(legacy)); } } @@ -203,7 +206,7 @@ public void everyRenameTargetIsAStoredKey() throws IOException { } } for (final String legacy : new String[] { "ProxyProtocol", - "KeepWwwPrefix", "KeepDoubleSlashes" }) { + "KeepWwwPrefix", "KeepDoubleSlashes", "Pause" }) { assertTrue(legacy + " resolves to no stored key", keys.contains(ProfileFormat.canonicalName(legacy))); } From 2e053adf027a1e2b65e78cc97d28626881b228fb Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 17 Aug 2026 22:09:54 +0200 Subject: [PATCH 2/3] Catch a rename that moves a key in one table but not the other The key tables are parallel: a stored field and the option mapper that turns it into an engine argument. Renaming one side only leaves the mapper matching no stored key, and the option stops emitting with nothing louder than a log line. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- .../httrack/android/WinProfileParityTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/test/java/com/httrack/android/WinProfileParityTest.java b/app/src/test/java/com/httrack/android/WinProfileParityTest.java index 1125f7be..ceb9b0bd 100644 --- a/app/src/test/java/com/httrack/android/WinProfileParityTest.java +++ b/app/src/test/java/com/httrack/android/WinProfileParityTest.java @@ -183,6 +183,27 @@ private static List serializerKeys() throws IOException { return keys; } + private static List mapperKeys() throws IOException { + final Matcher m = Pattern.compile( + "new Pair\\(\"([^\"]+)\"").matcher(mapperTable()); + final List keys = new ArrayList(); + while (m.find()) { + keys.add(m.group(1)); + } + assertEquals("mapper keys parsed", 94, keys.size()); + return keys; + } + + /* Renaming a key in one table only strands the other half: an option whose + mapper no longer matches a stored key emits nothing, in silence. */ + @Test + public void everyMapperKeyIsStored() throws IOException { + final List stored = serializerKeys(); + for (final String key : mapperKeys()) { + assertTrue(key + " maps an option no field stores", stored.contains(key)); + } + } + @Test public void keysUseTheWinHttrackSpelling() throws IOException { final List keys = serializerKeys(); From 5488061a2f0825dac35cfc2ec7d7ff96730d289e Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Mon, 17 Aug 2026 22:23:55 +0200 Subject: [PATCH 3/3] Let each key regex be the other's control, and drop the counts Both key lists were checked against a hardcoded 94, which only catches an entry the regex already matches: add one it misses and the count stays 94 while the new key goes untested. Count the declarations instead, and compare the two tables as sets, so a stored key with no mapper fails as loudly as a mapper under no stored key. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Xavier Roche --- .../httrack/android/WinProfileParityTest.java | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/app/src/test/java/com/httrack/android/WinProfileParityTest.java b/app/src/test/java/com/httrack/android/WinProfileParityTest.java index ceb9b0bd..1de53b4d 100644 --- a/app/src/test/java/com/httrack/android/WinProfileParityTest.java +++ b/app/src/test/java/com/httrack/android/WinProfileParityTest.java @@ -16,6 +16,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.Test; @@ -171,37 +172,46 @@ private static String mapperTable() throws IOException { return TestSources.javaSource("OptionsMapper"); } - private static List serializerKeys() throws IOException { - final Matcher m = Pattern.compile( - "new Pair\\(R\\.id\\.\\w+,\\s*\"([^\"]+)\"\\)") - .matcher(mapperTable()); - final List keys = new ArrayList(); - while (m.find()) { - keys.add(m.group(1)); + private static int occurrences(final String source, final String text) { + int count = 0; + for (int at = source.indexOf(text); at != -1; at = source.indexOf(text, + at + 1)) { + count++; } - assertEquals("serializer keys parsed", 94, keys.size()); - return keys; + return count; } - private static List mapperKeys() throws IOException { - final Matcher m = Pattern.compile( - "new Pair\\(\"([^\"]+)\"").matcher(mapperTable()); + /* Counting the declarations separately keeps a regex that quietly stops + matching from passing every key test on a short list. */ + private static List keysOf(final String declaration, + final String pattern) throws IOException { + final String source = mapperTable(); + final Matcher m = Pattern.compile(pattern).matcher(source); final List keys = new ArrayList(); while (m.find()) { keys.add(m.group(1)); } - assertEquals("mapper keys parsed", 94, keys.size()); + assertEquals(declaration + " entries parsed", + occurrences(source, declaration), keys.size()); return keys; } - /* Renaming a key in one table only strands the other half: an option whose - mapper no longer matches a stored key emits nothing, in silence. */ + private static List serializerKeys() throws IOException { + return keysOf("new Pair(R.id.", + "new Pair\\(R\\.id\\.\\w+,\\s*\"([^\"]+)\"\\)"); + } + + private static List mapperKeys() throws IOException { + return keysOf("new Pair(", + "new Pair\\(\"([^\"]+)\""); + } + + /* The two tables are halves of one wiring: a key stored with no mapper never + reaches the engine, and a mapper under no stored key never runs. */ @Test - public void everyMapperKeyIsStored() throws IOException { - final List stored = serializerKeys(); - for (final String key : mapperKeys()) { - assertTrue(key + " maps an option no field stores", stored.contains(key)); - } + public void everyStoredKeyHasAMapper() throws IOException { + assertEquals(new TreeSet(serializerKeys()), + new TreeSet(mapperKeys())); } @Test