Files
agendula/dav/PROVENANCE.md
T
makiolaj 5a81d3c2f5 sync(chunk 2a): vendor dav4jvm 2.2.1 as :dav
JitPack-only publishing conflicts with FAIL_ON_PROJECT_REPOS, artifacts are
unsigned and not immutable, and upstream shipped two breaking majors 19 days
apart. 2.2.1 is the last OkHttp release: 3.0.0 moved to Ktor and 4.x needs
Java 21, and SYNC.md's whole auth section is written in OkHttp terms, so 4.x
would have invalidated it. A plain JVM module, not an Android library — the
tree has no Android imports and keeping it that way keeps the floret-kit
extraction a file move.

Four changes from upstream, all in dav/PROVENANCE.md:
- commons-lang3 dropped (one import, one call). The replacement requires the
  whole string to be consumed, as DateUtils did: pattern 1 ends in a literal
  'GMT', so SimpleDateFormat would match "...GMT+02:00" as a prefix and throw
  the offset away.
- HTTP dates were parsed and formatted in the device's local zone — the GMT in
  the format string is a quoted literal and timeZone was never set, so every
  getlastmodified was out by the local UTC offset. Upstream tests dates
  nowhere. The formatter is also no longer shared and mutable.
- dav4jvm#209: permanent redirects now reach the caller via
  DavResource.permanentLocation, cleared per request so it never describes an
  earlier one.
- xpp3 is compileOnly; Android supplies org.xmlpull.v1.

SYNC.md's other claimed defect, "does not follow 303", is not true of 2.2.1 —
pinned by a test rather than removed from the notes.

CI names :dav:test explicitly; testDebugUnitTest is Android-only and would
have run none of the vendored suite. lintDebug added to the per-chunk checks
after it caught a literal byte-order mark left by chunk 1.
2026-09-04 17:12:27 +02:00

6.8 KiB
Raw Blame History

:dav — vendored dav4jvm

Upstream: bitfireAT/dav4jvm, tag 2.2.1, commit f434c9d19b322228916c106beaebd8634b85ddb1. Licence: MPL-2.0 (dav/LICENSE, verbatim). Every file keeps its upstream header, as §3.4 requires. Agendula's own code is MIT and unaffected — MPL is file-level copyleft, which is why this lives in its own module rather than inside :app.

62 source files, ~4,200 lines, plus upstream's 15 test classes.


Why vendored, and why this version

docs/SYNC-PLAN.md decision 1 settled vendor rather than depend. dav4jvm is published on JitPack only, which conflicts with our FAIL_ON_PROJECT_REPOS policy, does not sign artifacts, and rebuilds on demand — so a coordinate is not immutable. Upstream also shipped two breaking majors nineteen days apart (3.0.0 on 2026-07-08, 4.0.0 on 2026-07-27).

2.2.1 is the last OkHttp release. 3.0.0 deleted the OkHttp package for Ktor, and 4.x additionally requires Java 21 bytecode while we target 17 across :app and all of floret-kit. Taking 4.x would mean Ktor (~2.45 MB), the wrong guava flavour, and a Java-target migration — and it would invalidate the whole auth design in docs/SYNC.md, which is written in OkHttp terms throughout (preemptive Basic via an Interceptor, OkHttp stripping Authorization on cross-host redirects, BasicDigestAuthHandler because OkHttp has no Digest).

Vendoring at 2.2.1 costs us upstream's later work and makes us responsible for this tree. What it buys: our own Java target, no JitPack, no Ktor tail, no xpp3 in the APK, and the freedom to fix the defects below rather than route around them.

This is a plain JVM module, not an Android library. The upstream tree has zero Android imports and must not gain any: docs/SYNC.md earmarks this layer for floret-kit's core-dav, and Calendula needs the same primitives, so extraction should stay a file move.


Changes from upstream

Upstream's own test suite is vendored with the code and passes unmodified — that is what makes these changes safe to make. Our additions live in LocalChangesTest; every other test file is upstream's, untouched.

1. commons-lang3 removed

HttpUtils.kt imported org.apache.commons.lang3.time.DateUtils for exactly one call — DateUtils.parseDate(str, locale, patterns…), a loop over format strings. Replaced with that loop. The dependency is gone; the format list is byte-for-byte upstream's, comments included.

⚠️ The loop is not the whole of what DateUtils did. It parsed with a ParsePosition and rejected a pattern unless the entire string was consumed; SimpleDateFormat.parse(String) accepts a prefix. Pattern 1 ends in the quoted literal 'GMT', so "Wed, 21 Oct 2015 07:28:00 GMT+02:00" matches it as a prefix and the offset is silently discarded — a two-hour error, and precisely the failure change 2 exists to remove. The replacement requires full consumption.

2. ⚠️ HTTP dates were parsed and formatted in the device's local zone

An upstream defect, not a porting artefact. httpDateFormatStr is "EEE, dd MMM yyyy HH:mm:ss 'GMT'" — the GMT is a quoted literal, so SimpleDateFormat neither reads nor writes a zone from it, and httpDateFormat never had timeZone set. So formatDate emitted local time labelled GMT, and parseDate read 07:28:00 GMT as 07:28 local — every getlastmodified out by the device's UTC offset, in whichever direction the user happens to live.

Upstream's HttpUtilsTest covers only fileName() and never touches dates, which is why this survived. Fixed by forcing GMT on the formatter and on every parse attempt; patterns carrying a real z still take the zone from the input, as they must. Covered by LocalChangesTest.

3. Permanent redirects now reach the caller (dav4jvm#209)

followRedirects mutated location in place for every 3xx and told the caller nothing, so a caller could not distinguish "this resource has moved, store the new URL" from "follow this once". DAVx5 consequently never rewrites a stored collection URL after a 301 and re-follows it on every sync; docs/SYNC.md names persisting the new URL ourselves as the fix.

Added DavResource.permanentLocation, set only along an unbroken chain of 301 / 308. A temporary hop ends the chain — 301 → 302 means the resource moved to the 301's target and is being served elsewhere for now, so persisting the 302's target would be wrong. location still moves for every redirect, unchanged.

It is cleared at the start of every request, so it describes the request just made and never one made earlier through the same object. DavResource instances are reused, and a stale value would have the caller persist a URL that a later move() already superseded.

4. xpp3 is compile-time only

Upstream declares org.ogce:xpp3 as api. Android ships org.xmlpull.v1 in the framework, so the 371 KB jar is compileOnly here and never reaches the APK. The unit tests run on a plain JVM, which has no framework, so they get the real implementation via testImplementation.

5. httpDateFormat is no longer a shared mutable formatter

Upstream exposed a single public SimpleDateFormat. It is mutable and not thread-safe: two workers formatting a header concurrently corrupt each other through its Calendar, and any caller could setTimeZone on it and undo change 2 for everyone else. It is now private and built per call. Nothing else in the tree referenced it.


Build integration

  • :dav's tests are a plain JVM test task. CI runs testDebugUnitTest, which exists only on Android variants, so .forgejo/workflows/ci.yaml names :dav:test explicitly. Without that the vendored suite is compiled by nobody and run by nobody, and the safety argument above is void.
  • :app declares testImplementation(libs.xpp3). compileOnly is not transitive, :app's unit tests run on a plain JVM with no framework, and isReturnDefaultValues = true makes android.jar's stub factory return null — so anything touching XmlUtils would fail with an unrelated-looking NPE.

What was not a defect

docs/SYNC.md lists two dav4jvm defects we would inherit. Only one of them exists in this version:

  • "Handles 301/302/307/308 but not 303" — does not hold for 2.2.1. followRedirects gates on OkHttp's Response.isRedirect, which includes HTTP_SEE_OTHER, and it re-sends the same method, which is what RFC 6764 §5 asks for during discovery. Pinned by a test so a future resync cannot lose it silently.
  • dav4jvm#209 — real, and fixed above as change 3.

Resyncing

Fetch the new tag, diff against f434c9d, reapply changes 1–4, run ./gradlew :dav:test. If upstream's suite fails, the port is wrong — that is the entire reason it is vendored alongside the code.