Skip to content

PartFile: harden per-part hashing against gap/disk-state drift - #560

Merged
mrjimenez merged 2 commits into
amule-project:masterfrom
got3nks:fix/partfile-gap-tracking-vs-disk-drift
May 11, 2026
Merged

PartFile: harden per-part hashing against gap/disk-state drift#560
mrjimenez merged 2 commits into
amule-project:masterfrom
got3nks:fix/partfile-gap-tracking-vs-disk-drift

Conversation

@got3nks

@got3nks got3nks commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Surfaced in the follow-up thread on #302: when a partfile's logical length is shorter than what the gap-list claims is filled, per-part hash verification trips CEOFException -> SetStatus(PS_ERROR), requiring manual truncate -s <full size> to recover. Two narrow changes to make per-part hashing robust against gap-vs-disk-state drift.

Fix

1. Gate the async hash enqueue on IsComplete(part)CPartFileHashThread's main-thread enqueue point in FlushBuffer Phase 3 queues every dirty part regardless of whether the part's gap-list range is fully filled. The synchronous verify loop in ~CPartFile already gates on IsComplete (line 286); the async path lost that gate when the hash thread was introduced. Without it, a write that lands inside a part marks the part dirty and queues HashSinglePart even when the part still has gaps; HashSinglePart then walks past the highest written offset and trips PS_ERROR.

2. Defensive bound-check in HashSinglePart — pre-read, verify partfileLen >= offset + length. On mismatch, log + AddGap(offset, offset+length-1) + return false. The caller treats the part as corrupt and the gap-list opens those bytes for re-fetch, so recovery happens transparently on the next download attempt.

Together these cover the scenarios where gap-list and on-disk state can legitimately diverge — transient write failure followed by .met persistence and unclean shutdown, external truncation, partial backup restore, FS corruption — all of which previously required manual intervention.

Test

End-to-end live download of a 10 GB file from a LAN seeder, on macOS amuled built from this branch:

  • Throughput identical to master (135 MB/s peak, completes in ~90 s).
  • Final md5 matches the seeder byte-for-byte (7686fedd790b58955633d46df8e56523).
  • No spurious "reopening gap" log lines, no PS_ERROR, no EOF while hashing exceptions.

got3nks added 2 commits May 9, 2026 22:26
CPartFileHashThread's main-thread enqueue point in FlushBuffer Phase 3
queues every dirty part regardless of whether the part's gap-list range
is fully filled. The synchronous verify loop in ~CPartFile (line 286)
already gates on IsComplete; the async path lost that gate when the
hash thread was introduced.

Without it, a write that lands inside a part marks the part dirty and
queues HashSinglePart even when the part still has gaps. HashSinglePart
then reads PARTSIZE bytes from the partfile, walking past the highest
written offset on a partfile that is sized to "last write end + 1"
under the default sparse-no-preallocation behaviour on Linux/macOS,
and trips CEOFException -> SetStatus(PS_ERROR).

Restore the gate, mirroring the sync path: clear the dirty bit
unconditionally so the loop doesn't busy-spin on incomplete parts, and
trust that future writes to the same part will re-set it once the part
is gap-complete.
HashSinglePart trusts the gap-list unconditionally: it computes
offset = PARTSIZE * partnumber, length = GetPartSize(partnumber), and
hands them to CreateHashFromFile. The gap-list and the partfile's on-
disk length can diverge in several scenarios:

  - A write that the gap-list believed succeeded actually failed
    (ENOSPC / EIO / transient I/O), the .met got persisted before the
    retry, and the daemon was killed unclean before retry succeeded.
  - The partfile was truncated externally (FS hiccup, manual edit,
    partial backup restore).
  - aMule on Linux/macOS uses sparse-no-preallocation by default
    (CreateSparseFile ignores the size argument; partfile size grows
    only with writes, never reaches the target file size unless the
    last write happens to extend it).

In each case CreateHashFromFile reads past the partfile's actual length
and throws CEOFException, which the existing handler converts to
SetStatus(PS_ERROR). Recovery requires the user to truncate -s <full
size> the partfile manually before the file can resume.

Detect the divergence pre-read: when partfileLen < offset+length,
reopen the gap via AddGap and return false (caller treats the part as
corrupt and the gap-list opens those bytes for re-fetch). Recovery
then happens transparently on the next download attempt instead of
demanding manual intervention.
@mrjimenez
mrjimenez merged commit 130b0f4 into amule-project:master May 11, 2026
12 checks passed
@got3nks
got3nks deleted the fix/partfile-gap-tracking-vs-disk-drift branch May 11, 2026 08:01
got3nks pushed a commit to got3nks/amule that referenced this pull request Jul 24, 2026
* Fix alcc: preserve Unicode characters in ed2k links

The `CleanFilename()` function in alcc used a regular expression
`[^[:alnum:]_.-]` to replace every character not in the set of
alphanumerics, dot, underscore, and hyphen with an underscore.
This stripped all non‑ASCII characters (e.g., Chinese, Japanese,
Cyrillic) from filenames, turning them into a series of underscores
in the generated ed2k:// links, making them unreadable.

This commit rewrites `CleanFilename()` to replace only the three
characters that actually break the ed2k link format: `|` (field
separator), `/` (end‑of‑link marker), and `\` (path separator on
Windows). All other characters, including Unicode, are left
untouched.

As a result, `alcc` now produces fully readable ed2k links with the
original filenames, while still preventing malformed links. The
link integrity is maintained because the ed2k protocol identifies
files by their hash and size, not by the filename string. This
change has been tested with filenames containing Chinese characters
under a UTF‑8 locale (LANG=zh_CN.UTF‑8) on Debian GNU/Linux.

* Update ed2khash.cpp
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.

2 participants