Repository: golded-ftn-jam-python.
The distribution remains golded-ftn-jam; imports use golded_ftn_jam.
The source is public on GitHub. Version 1.3.0 is available on PyPI.
Install with Python 3.12 or newer:
python -m pip install golded-ftn-jam==1.3.0Read and edit JAM revision 1 areas through the golded-ftn models. Python 3.12+.
For development, clone core and the format package:
git clone https://github.com/golded-dev/golded-ftn-python.git
git clone https://github.com/golded-dev/golded-ftn-jam-python.git
cd golded-ftn-jam-python
uv sync --lockedfrom pathlib import Path
from tempfile import TemporaryDirectory
from golded_ftn import MessageBaseReader, ReaderOptions
from golded_ftn_jam import JamReader
# A minimal empty area. Real callers pass the area basename, without an extension.
with TemporaryDirectory() as directory:
base = Path(directory) / "example"
header = bytearray(1024)
header[:4] = b"JAM\0"
header[20:24] = (1).to_bytes(4, "little")
base.with_suffix(".JHR").write_bytes(header)
base.with_suffix(".JDT").write_bytes(b"")
base.with_suffix(".JDX").write_bytes(b"")
reader: MessageBaseReader = JamReader()
messages = list(reader.read(base, ReaderOptions(fallback_charset="CP850")))
assert messages == []The basename is exact; .JHR, .JDT and .JDX extensions are matched without
regard to case. Each must identify one regular file; symlinks and ambiguous
extension variants are rejected. The standalone reader ignores .JLR; editing
preserves it and creation initializes an empty file.
The index determines message numbers and which headers are live. Index holes and
deleted messages are skipped. Old unindexed headers are ignored. The reader
validates signatures, revision, record boundaries, offsets, subfield lengths,
message numbers and reused header offsets. By default it streams indexed
messages; pass ReaderOptions(eager=True) to load and validate the whole area
before returning a tuple. In eager mode a malformed later record cannot leave a
caller with a partial result. Filesystem errors remain filesystem errors;
damaged data and decoding errors raise ParserException with source path, byte
offset and a chained cause.
The standalone JamReader requires a stable area with no concurrent writes. It
does not lock the area or produce a snapshot across the three files. Stop the
writer or use a consistent copy before reading.
Decoding is strict, with core charset detection and CP850 fallback. Header FTSKLUDGE declarations are considered before body declarations. Conflicting charset declarations and IDs fail. Unknown charset names use the configured fallback. Mojibake repair is the caller's choice.
body_text contains only normalized .JDT text. Header control and routing
subfields go into control_lines, ahead of body metadata. Repeated controls and
routing entries keep their order within the core model's kludges, seen_by and
path sequences. Unknown subfields and nonzero HiID fields are bounds-checked
and skipped. The first originating/destination address is preserved as decoded
text, including node 0, point and domain. Other single-value subfields must agree
when repeated. Header MSGID/REPLYID take precedence over matching body IDs;
missing MSGID uses the core synthetic ID over decoded, normalized fields.
Dates are naive 1970-01-01 + DateWritten seconds, independent of the machine's
timezone; zero means None. TZUTC is retained as a control line. Writer and
recovery sessions invert that epoch to local wall time when a valid TZUTC offset
is present, so aware timestamps round-trip without an hour shift. The naive
DateWritten mapping is unchanged when TZUTC is absent. Raw attributes,
all three reply links and .JHR provenance are retained; zero reply links become
None. Unknown area metadata stays None.
Compressed, encrypted and escaped active messages are unsupported. Area discovery, databases, packing and repair are outside this package.
uv sync --locked --python 3.14
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run python -m mypy.stubtest golded_ftn_jam
uv build
uv run twine check dist/*
uv run python scripts/verify_distribution.pyDevelopment and distribution use public dependency constraints; no local source overrides are configured. Install unpublished dependencies from the reviewed candidate wheelhouse. See CONTRIBUTING.md and release checks.
- JAM-001 revision 1
- GoldED JAM structures
- The sibling
laravel-ftn-jam/src/JamReader.phpand its reader tests informed model mapping. Its sequential scan and permissive truncation are not used.
JAM(mbp) - Copyright 1993 Joaquim Homrighausen, Andrew Milner, Mats Birch, Mats Wallin. ALL RIGHTS RESERVED.
The package code is MIT licensed; the JAM specification has its own terms.
Strict reading remains the default. Archive mode requires a report callback:
from golded_ftn import ReaderIssue, ReaderOptions
issues: list[ReaderIssue] = []
options = ReaderOptions(archive_mode=True, on_issue=issues.append)
# Pass options to JamReader().read(source, options).Issues carry recovered, skipped or stopped, the actual filename, record
identity and physical offset. Their detail contains no message contents. A stop
means the traversal is incomplete; a validated prefix may still be returned.
Multiple issues can describe one record, including recovery followed by a skip.
Filesystem errors and callback exceptions propagate. Files must remain stable.
Bounded fields exceeding JAM's specification limits are retained and reported. Malformed TZUTC metadata is retained without interpretation. Distinct PID, FLAGS and TZUTC subfields stay in source order. Conflicting names, subjects, IDs or charset declarations are skipped rather than guessed. Failed records are skipped using the next fixed index slot; reused header offsets stop traversal.
If declared ASCII cannot decode a payload, the configured fallback is tried strictly and reported. The original charset control stays unchanged. Other decoding failures are skipped; there is no lossy decoding or mojibake repair.
JamWriter.create(base) creates .JHR, .JDT, .JDX and an empty .JLR.
Pass JamWriterOptions(extension_case="lower") to create .jhr and friends
instead. Open and read already use the casing that is on disk; both casings
of the same sidecar at once are still rejected. Existing files are refused.
The initial message number is 1.
from golded_ftn import MessagePatch, OutgoingMessage
from golded_ftn_jam import JamWriter
writer = JamWriter()
writer.create("new-area")
with writer.open("new-area") as session:
result = session.append(
OutgoingMessage(
from_name="Alice",
to_name="Bob",
subject="Hello",
body_text="Hello Bob",
)
)
result = session.update(
result.identity, MessagePatch(subject="Changed"), result.revision
)
session.delete(result.identity, result.revision)Every operation rereads and validates the index, referenced headers, text ranges
and active-message count under the byte-0 .JHR record lock. A session read returns
SessionMessage with a raw-byte SHA-256 revision. Changes to other messages do not
invalidate it. The revision includes identity, physical index/header/text locations
and SHA-256 over raw header, subfield and text bytes. An omitted patch field
retains its raw metadata; explicit None clears optional metadata. Names, subject, text and attributes reject None.
control_lines replaces general controls in both header subfields and inline
text. MSGID, address controls and routing retain their separate fields when
omitted; conflicting explicit controls are rejected. external_id and routing
patches remove obsolete inline copies as well as replacing their header metadata.
Body-only changes retain existing inline controls and routing. Unknown subfields
and nonzero HiID values survive unless their supported field is explicitly changed.
Content updates append a header, subfields and text, then redirect the index. Header-only changes retain the text bytes and record position. Unknown subfields, reserved words, timestamps and reply links remain intact. Delete marks the header, sets the recipient index CRC to FFFFFFFF, and decrements the active count. Message numbers and lastread data remain unchanged.
CP850 is the default. Encoding is strict; conflicting charset declarations, truncation, unsupported compression flags and arbitrary reply lists are refused. No MSGID, routing or duplicate detection is generated. Full-file snapshots support in-place rollback during ordinary I/O failures; rollback failure poisons the session. This provides no process-kill or power-loss transaction guarantee.
JamWriter.recovery_session(path, recovery_root=...) holds the .JHR byte-0 lock
across prepare/apply/settle and reuses that descriptor for header I/O. Recovery
covers .JHR, .JDT and .JDX only; .JLR is never touched. Torn header or
index bytes are restored from durable artifacts before ordinary validation.
concurrent=True is accepted.
Candidate interoperability is bounded to GoldED source commit d7449053bd674df54c4a442c92dce8c06ad7b586,
version 3.1.0, native macOS arm64/GCC 16 and Ubuntu 24.04 aarch64/GCC 13.
The suite release manifest pins the resulting binaries and records actual checks.
Other GoldED versions, Windows and network filesystems are not qualified.
Writers default to offline operation. Explicit concurrent=True opts into the
tested read/write/refresh/lastread/stale-save profile, not a general multiwriter
guarantee. Standalone readers still need a stable base; a session read uses its
format lock. MSG/Opus and Goldbase have no shared GoldED writer lock: stale-save
detection cannot close a race between comparison and a foreign write. Keep
same-message edits serialized. Goldbase is outside the Python C1 profile.
Use format-specific WriterOptions extension_case="lower" for newly created Unix bases used by GoldED; preserve existing casing and reject ambiguous twins. Hudson scan queues belong in the configured system directory. Recovery sessions have a durable journal protocol; ordinary open() sessions only roll back handled I/O failures. UNKNOWN outcomes retain artifacts for operator reconciliation.
The reference checkout is golded-open-source, commit
600266252b73174ff5116cee697ef9a97aeb1859. It contains
goldlib/gmb3/gmojamm.h (JamHdrInfo,
JamHdr, JamIndex, subfield IDs), gmojamm2.cpp (open_area initialization,
scan indexing), and gmojamm4.cpp (lock, unlock, save_message).
lock takes byte 0 length 1 in .JHR; save_message maintains recipient/MSGID/
REPLY CRCs, active count and modification counter. Its CRC seed and missing final
complement are confirmed by goldlib/gall/gcrcs32.cpp::strCrc32.
These source checks establish layout and write semantics. They do not establish
that a running GoldED reader refreshes safely after external changes.
Add recoverable sessions and explicit extension_case. Retain the JHR lock descriptor across reads and mutations; preserve raw metadata and revisions.
Linux and macOS, Python 3.12–3.14, are the release targets. Windows is outside this candidate's test and compatibility promises. See release preparation.