Skip to content

feat: optional fragment on redirects, per kelson42's dirent-parameter design - #1125

Open
soloturn wants to merge 75 commits into
openzim:resolveTargetURLfrom
soloturn:feat/redirect-fragment
Open

feat: optional fragment on redirects, per kelson42's dirent-parameter design#1125
soloturn wants to merge 75 commits into
openzim:resolveTargetURLfrom
soloturn:feat/redirect-fragment

Conversation

@soloturn

Copy link
Copy Markdown

Alternative to this PR's resolveTargetURL/UrlUtils approach, implementing the design @kelson42 actually asked for:

The topic has been again discussed live in the last days, and here is the clear requirements my sides:

  • mini-html redirections are a workaround and as such we are no[t] building anything sustainable around them
  • there is in particular no requirement to have a "smart" libzim around mini-html
  • since the very beginning of the libzim, an effort has been made to have the libzim pretty agnostic about the type of data save[d] in it. It is important to not build anything related to mini-html in the libzim (for this reason as well)

and, from the linked issue:

  • Against any modification of the meaning of # in the path...
  • We have a way to store the fragment string in the corresponding dirent parameter and we should use that. Very important is to start a proper/robust (json?) structure in this dirent parameter...
  • We should not create a new type or new naming of redirects here. We should just allow to get/set an optional fragment with the current standard redirection related API primitives.

What this does instead

  • No path parsing, no #/? component splitting, nothing that gives those characters special meaning in a ZIM path.
  • No mini-HTML inspection or "smart" resolution anywhere in libzim.
  • The redirect dirent's existing parameter field (present in the format from the start, always empty for redirects until now) optionally carries the fragment, as a minimal tagged record (1-byte tag + 1-byte length + value) rather than pulling in a JSON dependency for one optional string — a tag this version doesn't recognize is skipped, not misread, so a future record type (e.g. Feature request: Add keyword/tag support #317's keywords) can share the field without breaking older readers.
  • No new redirect type. Creator::addRedirection() gained one new optional trailing parameter (targetFragment, default ""); Entry gained one new method (getRedirectFragment()) alongside the existing getRedirectEntry()/getRedirectEntryIndex().

A redirect that doesn't use a fragment is byte-for-byte identical to one written before this existed — encodeRedirectParameter("") returns "", the same empty parameter every redirect has always had.

Worth a maintainer's eyes on

Storing the fragment as a shared_ptr<const string> on writer::Dirent (not an inline string, not unique_ptrDirent is copy-constructed into DirentPool's storage, which rules out move-only types) still grows Dirent by one pointer's worth of bytes — 29 → 45 on 64-bit here — for every dirent, not just redirects with fragments. DirentTest.size exists specifically because this matters at the scale of a large ZIM's entry count, so flagging this plainly rather than glossing over it.

Verification

  • Full test suite: 31/31 passing.
  • Coverage: confirmed via gcovr (llvm-cov gcov backend) that every line this diff touches is exercised — 100% on src/writer/_dirent.h; on the other touched files, the only uncovered lines reported are pre-existing code outside this diff (checked by line number against the actual patch, not just the file-level percentage).
  • New tests: codec round-trip/edge-cases (test/tooltesting.cpp), writer/reader dirent round-trip with and without a fragment (test/dirent.cpp), and an end-to-end CreatorArchiveEntry test (test/archive.cpp).

Not attempted

Anything depending on inspecting item content or interpreting #/? as meaningful path characters — both are exactly what's ruled out above, so nothing from this PR's UrlUtils/mini-HTML-parsing approach carries over. The mini-HTML workaround remains the only option for readers that don't understand this new fragment field yet, matching @kelson42's stated interim plan on #1001.

Related: #1001, #317 (future dirent-parameter extension mentioned there).

kelson42 and others added 30 commits May 9, 2026 10:48
Fix Typos in ICU dep part of meson.build
This reduces the risk of losing the main results of ZIM creation because
of an IO error during the checksum computation step.
Now, in such a situation the checksum can be added to the quasi-invalid
ZIM file by a simple external script.
zim::writer::BinaryFile is introduced with the purpose of encapsulating
all file operations (currently happening via syscalls on a file
descriptor) in a class, that, upon completion, can be trivially
reimplemented via buffered IO.
Didn't apply the new method in src/writer/cluster.cpp since it is used a
little differently there.
This slightly changes the chunked writing of large non-compressed
clusters:

- the chunk size is reduced from 4GiB to 16MiB,
- and a failure to fully write the current chunk results in an exception
kelson42 and others added 29 commits June 11, 2026 17:15
…estions_for_xapian_2.0

Force single-letter suggestions for Xapian 2.0
The non-orchestrated flow in the Log.concurrencyOrchestration unit-test
is unstable by its nature due to the absence of any guarantees on the
order of unsynchronized concurrent operations performed in different
threads. Suspensions of threads by a given duration only increase the
probability of achieving the desired scenario, with longer wait times
having higher chances of working as intended even on slow hardware or
under high load.
A little stabler unit-test Log.concurrencyOrchestration
Previous implementation of millisleep() could overshoot significantly
when the thread was not given enough CPU time. Suppose that the code is
running in a high contention environment, competing for CPU time with a
lot of other processes/tasks, so that the thread only wakes up once
every N milliseconds. Then a call to the old implementation of
millisleep(T) could take N*T milliseconds!

With the new implementation the same call will take at most T+N+O(1)
milliseconds (where O(1) stands for the fixed duration that
it takes to enter and exit the function; it applies to the old
implementation too, but there it was not worth mentioning).
Fixed the test utility millisleep()
Rename to .zim file, after the checksum append
Made the stemmer set-up code identical across
SuggestionDataBase::initXapianDb() and
XapianDbMetadata::XapianDbMetadata().
This is a quick fix to zimcheck's output being polluted by libzim's
own unsolicited messages.

Writer code keeps printing to stdout but it's less of a concern
…lution

Quick fix for pollution of zimcheck output
…arameter

Implements the design kelson42 settled on for openzim#1001 (redirect with
fragment support), replacing the approach in openzim#1082 ([WIP] Support for
URL redirects / resolveTargetURL), which kelson42 rejected on this
thread and on the issue:

  openzim#1001 (comment)
  openzim#1082 (comment)

His stated requirements, and how this meets each of them:

- "Against any modification of the meaning of # in the path" -- this
  adds no path parsing at all. No '#'/'?' component-splitting utilities.
- "We have a way to store the fragment string in the corresponding
  dirent parameter and we should use that." -- the redirect dirent's
  existing "parameter" field (present in the format since the
  beginning, always empty for redirects until now) now optionally
  carries the fragment.
- "Very important is to start a proper/robust (json?) structure in this
  dirent parameter... allow further extensions in the future" -- rather
  than pull in a JSON dependency for one optional string, the parameter
  is a minimal tagged (TLV) record: 1 byte tag + 1 byte length + value.
  A tag this version doesn't recognize is skipped, not misread, so a
  future record type (e.g. openzim#317's keywords) can share the same field
  without breaking older readers.
- "We should not create a new type or new naming of redirects... just
  allow to get/set an optional fragment with the current standard
  redirection related API primitives." -- Creator::addRedirection()
  gained one new optional parameter (targetFragment, default ""), and
  Entry gained one new method (getRedirectFragment()) alongside the
  existing getRedirectEntry()/getRedirectEntryIndex(). No new redirect
  type, no "smart" resolution, no mini-HTML inspection anywhere in
  libzim.

A redirect that doesn't use a fragment is byte-for-byte identical to
one written before this feature existed: encodeRedirectParameter("")
returns "", exactly the empty parameter every redirect has always had.

## Changes

- src/tools.h/.cpp: encodeRedirectParameter()/decodeRedirectFragment(),
  the TLV codec described above, MAX_REDIRECT_FRAGMENT_SIZE (253 bytes
  = the 255-byte on-disk parameter limit minus the record's own 2-byte
  header).
- src/writer/_dirent.h, src/writer/dirent.cpp: writer::Dirent gained
  set/getRedirectFragment() and now actually writes non-zero parameter
  bytes for the first time (this field existed in the format but the
  writer always hardcoded its length to 0). Stored as
  shared_ptr<const string>, not an inline string: DirentTest.size exists
  specifically because Dirent's in-memory size matters at the scale of
  the millions of entries a large ZIM can have, and a field almost no
  dirent uses shouldn't cost more than a pointer on the ones that don't
  (shared_ptr, not unique_ptr, because Dirent is copy-constructed into
  DirentPool's storage - confirmed by an actual compile error when I
  first tried unique_ptr). This does grow Dirent by one pointer's worth
  of bytes (29 -> 45 on 64-bit here) for every dirent, not just
  redirects with fragments; flagging this plainly since it's a real,
  measured memory cost of the feature, worth a maintainer's eyes on the
  tradeoff.
- include/zim/writer/creator.h, src/writer/creator.cpp:
  addRedirection() gained targetFragment as a new trailing defaulted
  parameter (after hints, not before, to avoid an overload-resolution
  ambiguity against braced-init-list Hints arguments already used
  throughout the existing test suite - confirmed by hitting that exact
  ambiguity with a first attempt that added a new overload instead).
- include/zim/entry.h, src/entry.cpp: Entry::getRedirectFragment(),
  same InvalidType-on-non-redirect convention as the existing
  getRedirectEntryIndex().
- test/tooltesting.cpp: encode/decode round-trip, empty-fragment,
  overlong-fragment-throws, and forward-compatibility (unknown record
  tag skipped, truncated parameter degrades to "" rather than crashing)
  tests for the codec directly.
- test/dirent.cpp: writer/reader round-trip test with a fragment set,
  a same-as-before-this-feature round-trip test with none, and an
  updated DirentTest.size (see above -- the Windows/32-bit constants
  are recalculated from sizeof(shared_ptr) on those platforms, not
  measured; only the 64-bit non-Windows value has actually been run).
- test/archive.cpp: end-to-end Creator -> Archive -> Entry test
  (redirectWithFragment) covering a redirect with a fragment, one
  without, and the InvalidType-on-non-redirect case.

## Verification

Full test suite (31/31) passes. Confirmed via gcovr (llvm-cov gcov
backend) that every line touched by this diff is exercised: 100% on
src/writer/_dirent.h, and on src/entry.cpp/src/tools.cpp/
src/writer/creator.cpp/src/writer/dirent.cpp the only uncovered lines
reported are pre-existing code untouched by this diff (verified by
line number against the actual patch, not just the file-level percentage).

## Not attempted

Anything from openzim#1082 that depended on inspecting item content (mini-HTML
detection/resolution) or interpreting '#'/'?' as meaningful path
characters - both are exactly what kelson42's comments rule out, so
none of that carries over here. The mini-HTML workaround remains the
only option for readers that don't yet understand this fragment field,
same as kelson42's stated interim plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kelson42

Copy link
Copy Markdown
Contributor

@soloturn Can we have a PR which does only things related to the issue and nothing else please?

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.

6 participants