Skip to content

Saving a project writes back settings nobody chose - #127

Closed
xroche wants to merge 5 commits into
masterfrom
fix/absent-keys
Closed

Saving a project writes back settings nobody chose#127
xroche wants to merge 5 commits into
masterfrom
fix/absent-keys

Conversation

@xroche

@xroche xroche commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Loading a project seeds Android's defaults, then lays the profile on top. serialize() wrote a line for every key it knows, using an empty value where the map held nothing, so every key came back present after a save.

Absent and empty are different things across front ends: WinHTTrack applies its own default for a missing key and takes an empty one literally. Opening a WinHTTrack project on the phone, changing nothing and saving was enough to leave our footer template and filter list in the file. The next desktop crawl then used them.

A key is now written only when the next load would not put it back by itself. The baseline for that is the map snapshotted once resetMap() has finished, not the built-in fieldsDefaults table: what refills an absent key is initializeMap() plus loadDefaultPreferences(), so a user with saved default options would otherwise lose settings. The first version of this branch made exactly that mistake, and the second commit is the fix. Any key the loaded profile carried is kept whatever it holds, so saving can only stop adding keys and never start removing someone else's line.

One consequence worth naming: a key left at the value the load rebuilds is no longer pinned in the file, so a project follows a later change to that baseline instead of holding the old value. For Footer that is what #122 wanted anyway.

Closes #126

Loading a project seeds Android's defaults and lays the file over the top,
and serialize() then wrote a line for every key it knows, empty where the
map held nothing. So every key came back present after a save, carrying our
default or an empty value.

Absent and empty differ to the other front ends: WinHTTrack applies its own
default for a missing key and takes an empty one literally, so opening a
WinHTTrack project on the phone and saving it left our footer template and
our filter list behind. Write a key only when its value differs from the
seeded default, which keeps a field the user deliberately cleared while
leaving a setting nobody chose out of the file.

initializeMap() and the writer now read the same defaults from one method,
so the baseline being compared against cannot drift from the one seeded.

Closes #126

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHQdRcxpBhjGhdKwwmtj36
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche marked this pull request as draft August 17, 2026 13:08
@xroche

xroche commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Holding this as a draft: the review found the omission baseline is the wrong one, and the result is silent corruption of a saved setting.

toFile compares each value against seededDefaults(), the built-in table. What actually refills an absent key on reload is resetMap(), which is initializeMap() plus loadDefaultPreferences(), so the two baselines part company as soon as a user has saved their own default options.

Repro, no code needed. Set Max transfer rate to 5000 and save it as the default options. Open a project, set the rate to 25000, save. That equals the built-in default, so MaxRate is dropped from the file. Reopen the project: resetMap() seeds 25000 and then the saved preference overwrites it with 5000, and the file has no line to correct it. The project now runs at 5000, and the crawl gets -A 5000.

Every key in fieldsDefaults is exposed the same way, checkboxes included. AcceptLanguage fails for a second reason: its default is derived from the device locale at call time, so a project written on a French device drops the key and reloads as en,* after a language change.

The fix has two parts, neither in this diff. Compare against the state the project was actually seeded with, snapshotted after resetMap() rather than rebuilt from the static table. And keep any key the loaded profile already contained, which unserialize already knows and currently throws away, so that opening a foreign profile can only stop adding keys and never start removing them.

Reworking on that basis.

xroche and others added 4 commits August 17, 2026 15:27
The first cut of this fix omitted a key when its value matched the built-in
fieldsDefaults table. That is the wrong baseline: what refills an absent key
is resetMap(), which is initializeMap() plus loadDefaultPreferences(), so a
user with saved default options loses settings. Set 5000 as the default
transfer rate, set 25000 in a project, save, reopen: the key matched the
table and was dropped, and the project comes back at 5000 with the crawl
getting -A 5000. AcceptLanguage failed a second way, its default being
derived from the device locale at call time.

Snapshot the map once resetMap() has finished, and compare against that.
Keep every key the loaded profile carried, whatever it holds, so saving can
only stop adding keys and never start removing someone else's line; the
profile on disk supplies that set when an activity restart has left the map
without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHQdRcxpBhjGhdKwwmtj36
Signed-off-by: Xavier Roche <roche@httrack.com>
Two ways the previous commit still lost a setting.

The crawl-start write opens winprofile.ini in append mode to hold its lock,
and the reader takes the last line for a key, so a key left out of that
write kept whatever an earlier block said. Build the settings first, then
truncate under the lock and write them, which also stops the file growing a
block per crawl.

The baseline was snapshotted after resetMap(). OptionsActivity holds its own
mapper, so resetting the saved default options there changed what a future
load rebuilds while this snapshot stayed put, and a value matching the old
preference was dropped and came back as the built-in default. Derive the
baseline from the defaults and the saved options on each call instead, and
let loadDefaultPreferences() build the map from the same method so the two
cannot disagree.

AcceptLanguage comes from the device locale rather than a fixed default, so
it is always written: the device can change between saves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHQdRcxpBhjGhdKwwmtj36
Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

# Conflicts:
#	app/src/test/java/com/httrack/android/WinProfileParityTest.java
@xroche

xroche commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Closing this. It needs a different design, not another commit on the branch.

The rule omits a key when Android's own next load would rebuild the same value. That baseline includes the device's saved default options, so what gets written into a file WinHTTrack and WebHTTrack also read depends on the phone's own preferences: two devices write different files from the same project. The shared key table, now vendored at app/src/main/jni/httrack/winprofile-keys.tsv, declares per key what an absent line means, and four rows say an empty value is literal. This branch assumes the opposite, and its own test asserts that dropping an empty UserID is the desired outcome.

Two other defects found in review, recorded so nobody rediscovers them. A key the profile carried that Android has no field for is dropped on save: heldKeys records the name, nothing records the value, and both the value map and the writer loop iterate fieldsSerializer. That became true on the crawl path here because the write changed from append to truncate. Separately, a failed write leaves the profile empty, since the truncate succeeds before the bytes are written.

The branch stays for reference. #126 stays open with a smaller approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Saving a project writes every option, so keys the profile never had come back set

1 participant