Skip to content

4.0.0 release prep - #7

Merged
taplin merged 5 commits into
mainfrom
release-prep
Oct 13, 2026
Merged

taplin merged 5 commits into
mainfrom
release-prep

Conversation

@taplin

@taplin taplin commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Release prep for Perfect-XML 4.0.0, the first release since 3.1.3 (2018) and the Swift 6 rewrite. Perfect-FileMaker 4.0.0 depends on it, so this goes first.

  • iOS 15 declared, plus CI: Linux Swift 6.2/6.4 and macOS Xcode 26.0.1/26.6, each in debug and release with warnings as errors, and an iOS Simulator job.
  • XXE hardening. XDocument builds its own parser context with entity substitution, DTD loading and external subsets forced off, whatever libxml2's (per-thread) globals say. Before, turning on xmlSubstituteEntitiesDefault(1) let a file:// entity's contents leak through nodeValue. XML_PARSE_NONET stays on as defence in depth. XMLStream no longer loads external DTDs; 3.1.3 had turned that on by mistake through a constant from the wrong enum.
  • SAXParser no longer traps on non-ASCII text.
  • XMLEncoder:
    • namespace: now writes a well-formed xmlns attribute (it produced malformed XML before).
    • Arrays and nested containers throw XMLEncoderError instead of encoding silently or calling fatalError.
  • README corrections:
    • The macOS brew link --force libxml2 advice caused the "conflicting types" build failure, so it's gone.
    • The README claimed Codable decoding, which doesn't exist; that claim is removed.
    • A "Migrating from 3.x" section is added.
  • Removed: the unused XMLDecoderError and a stale PADockerfile. Added a .yum provider.

Testing

30/30 with warnings as errors in every run:

  • Xcode 27 and Xcode 26.3, debug and release;
  • Linux 6.2 and 6.4;
  • iOS Simulator on Xcode 27 and 26.3.

The final verification also checked:

  • parser output is byte-identical to the previous code across 22 encoding, BOM, namespace and malformed inputs, except the intended XXE case;
  • 0 leaks over 84,000 parses;
  • a consumer depending on from: "4.0.0" builds on macOS and Linux.

After merge: tag 4.0.0 (needed before Perfect-FileMaker's PR can go green).

🤖 Generated with Claude Code

taplin and others added 5 commits October 6, 2026 08:45
The package builds for iOS with the SDK's libxml2 and all 26 tests pass on
an iPhone 17 simulator with Xcode 27, so declare .iOS(.v15) alongside
macOS 12. With the declaration, Xcode 27 builds the library at iOS 15; it
builds the test bundle itself at iOS 17. The manifest comment pointed at a
Documentation/ file that never existed; it now explains the Apple-platform
pkg-config choice inline.

CI is modeled on Perfect-CRUD's: Linux Swift 6.2 and 6.4 (with
libxml2-dev and pkg-config from apt) and macOS Xcode 26.0.1 and 26.6, each
in debug and release with warnings as errors, plus the tests on an iOS
Simulator after waiting for it to appear. The workflow itself has not run
on GitHub yet.

Verified locally: warning-free and 26/26 tests in debug and release on
Xcode 27 (Swift 6.4), Xcode 26.3 (Swift 6.2.4), and Linux swift:6.2-noble
and swift:6.4-noble containers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Install snippet uses from: "4.0.0"; requirements list macOS 12+, iOS 15+
  and Linux.
- The macOS notes told readers to brew install and force-link libxml2,
  which is the setup the manifest avoids (the SDK copy is used, and mixing
  Homebrew headers with the SDK's breaks the build). Replaced.
- The Codable section claimed XML decoding into Codable types; the package
  only has a flat-struct XMLEncoder. Corrected.
- Adds a "Migrating from 3.x" section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entities: libxml2 seeds a parser context's entity substitution from the
process-wide xmlSubstituteEntitiesDefault global, and xmlReadMemory keeps
it. With the global on, XDocument returned a file:// entity's contents
through nodeValue even though XML_PARSE_NOENT wasn't passed (reproduced on
macOS libxml2 2.9.13 and Ubuntu noble 2.9.14). XDocument now creates the
parser context itself and forces substitution, NOENT and external DTD
loading off before parsing; XML_PARSE_NONET stays as defence in depth.
XMLStream sets the reader's SUBST_ENTITIES and LOADDTD properties to 0 as
well (it didn't leak in testing, so that part is belt and braces). The
module header now includes parserInternals.h for
xmlCreateMemoryParserCtxt.

The old "XXE regression" test used a file:// entity, so it never tested
NONET, and it passed with the global off whatever the code did. It is
replaced by two tests: one with default globals, and one that turns
xmlSubstituteEntitiesDefault on (restoring it afterwards) and checks
XDocument (tree and nodeValue), XMLStream and HTMLDocument. The second
fails with the previous xmlReadMemory code.

XMLEncoder: namespace: wrote "<root:urn:example>...</root>", which isn't
XML. It now writes a default namespace declaration,
<root xmlns="urn:example">. A top-level array (even an empty one, which
encoded silently as <root/>) and nested containers now throw
XMLEncoderError. The unused public XMLDecoderError is removed (there is no
decoder). Adds the first XMLEncoder tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Adds a .yum(["libxml2-devel"]) provider for the Linux libxml2 target.
- Removes the stale Perfect Assistant PADockerfile.
- README: the security note no longer credits XML_PARSE_NONET with
  stopping XXE (file:// entities aren't network fetches); it describes
  forced-off entity substitution, with NONET as defence in depth. Only the
  document, node and parser classes are Sendable, not XMLEncoder. The
  encoder's namespace and array behaviour and the removed XMLDecoderError
  are documented, and the migration list is corrected.
- CI: Xcode builds the test bundle above the package's iOS 15 target
  (iOS 17 with Xcode 27); only the library is built at iOS 15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…builds

The comments, test comment and README called it process-wide. In threaded
libxml2 builds (macOS 2.9.13, Ubuntu noble 2.9.14) it is per-thread. The
fix is unchanged: Swift tasks can run on any thread, so the parsers still
force substitution off on their own contexts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@taplin
taplin merged commit c019dad into main Oct 13, 2026
18 checks passed
@taplin
taplin deleted the release-prep branch October 13, 2026 16:37
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.

1 participant