golded-ftn-jam 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- golded_ftn_jam-1.2.0/.gitignore +8 -0
- golded_ftn_jam-1.2.0/CHANGELOG.md +24 -0
- golded_ftn_jam-1.2.0/CONTRIBUTING.md +22 -0
- golded_ftn_jam-1.2.0/LICENSE +21 -0
- golded_ftn_jam-1.2.0/PKG-INFO +227 -0
- golded_ftn_jam-1.2.0/README.md +207 -0
- golded_ftn_jam-1.2.0/SECURITY.md +14 -0
- golded_ftn_jam-1.2.0/docs/release.md +86 -0
- golded_ftn_jam-1.2.0/pyproject.toml +46 -0
- golded_ftn_jam-1.2.0/scripts/sdist_hook.py +33 -0
- golded_ftn_jam-1.2.0/scripts/verify_distribution.py +141 -0
- golded_ftn_jam-1.2.0/src/golded_ftn_jam/__init__.py +4 -0
- golded_ftn_jam-1.2.0/src/golded_ftn_jam/py.typed +0 -0
- golded_ftn_jam-1.2.0/src/golded_ftn_jam/reader.py +489 -0
- golded_ftn_jam-1.2.0/src/golded_ftn_jam/writer.py +729 -0
- golded_ftn_jam-1.2.0/tests/test_archive.py +112 -0
- golded_ftn_jam-1.2.0/tests/test_reader.py +605 -0
- golded_ftn_jam-1.2.0/tests/test_writer.py +478 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.2.0 — 2026-10-05
|
|
4
|
+
|
|
5
|
+
- Add offline create/read/append/update/delete sessions, message revisions, record locks and rollback.
|
|
6
|
+
- Preserve raw subfields and maintain JAM indices, CRCs and counters.
|
|
7
|
+
- GoldED coexistence remains disabled pending build-specific integration tests.
|
|
8
|
+
|
|
9
|
+
- Replace controls and MSGID across JHR/JDT placements; preserve omitted structured fields and reject conflicting controls.
|
|
10
|
+
|
|
11
|
+
## 1.1.0 — Unreleased
|
|
12
|
+
|
|
13
|
+
Add reported archive reading: keep bounded oversized subfields and malformed
|
|
14
|
+
TZUTC metadata, retain distinct PID/FLAGS/TZUTC controls, try configured fallback
|
|
15
|
+
for mislabeled ASCII, and skip failed indexed records. Ambiguous structure stops
|
|
16
|
+
with a report. Strict reading remains the default.
|
|
17
|
+
|
|
18
|
+
## 1.0.0
|
|
19
|
+
|
|
20
|
+
- Read indexed JAM revision 1 areas with strict binary and decoding checks.
|
|
21
|
+
- Preserve message text, supported subfields, reply links and provenance.
|
|
22
|
+
- Ship typed public exports and independent synthetic fixtures.
|
|
23
|
+
- Report charset conflict offsets against stored subfield bytes.
|
|
24
|
+
- Keep development-only uv sources out of source distributions.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Use Python 3.12+ and uv with the sibling `golded-ftn` checkout. Run the checks in
|
|
4
|
+
README before proposing a change. Keep runtime dependencies limited to core.
|
|
5
|
+
|
|
6
|
+
Build fixtures from the JAM specification, independently of reader helpers.
|
|
7
|
+
Use synthetic messages; keep private archives out of tests and distributions.
|
|
8
|
+
Verify changed behavior through `JamReader.read`, including failures and offsets.
|
|
9
|
+
|
|
10
|
+
Keep this package focused on JAM reading and editing. Area discovery, databases,
|
|
11
|
+
packing and repair belong outside this package. Generated AGENTS.md comes from agent-compose.toml and the
|
|
12
|
+
local project fragment; edit those sources and preview, build, check.
|
|
13
|
+
|
|
14
|
+
Protect writer behavior through the public create/open/read/append/update/delete
|
|
15
|
+
API and independent raw records. Check omitted patch fields, explicit clearing,
|
|
16
|
+
controls in each supported physical placement, reply structures and unrelated
|
|
17
|
+
message revisions. Use controlled helper processes for lock conflicts and the
|
|
18
|
+
internal I/O seam for write, truncate, flush and rollback failures. Never reopen
|
|
19
|
+
the lock file while its operation lock is held. Preserve existing lastread data.
|
|
20
|
+
|
|
21
|
+
Keep GoldED closed during editing. A matching write lock alone does not prove
|
|
22
|
+
safe concurrent reads or refresh; build interoperability remains deferred.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GoldED.dev contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: golded-ftn-jam
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Strict reader and offline writer for JAM revision 1 message areas
|
|
5
|
+
Project-URL: Repository, https://github.com/golded-dev/golded-ftn-jam-python
|
|
6
|
+
Project-URL: Issues, https://github.com/golded-dev/golded-ftn-jam-python/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/golded-dev/golded-ftn-jam-python/blob/main/CHANGELOG.md
|
|
8
|
+
Author: GoldED.dev contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: golded-ftn<2,>=1.2.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# golded-ftn-jam
|
|
22
|
+
|
|
23
|
+
Repository: [`golded-ftn-jam-python`](https://github.com/golded-dev/golded-ftn-jam-python).
|
|
24
|
+
The distribution remains `golded-ftn-jam`; imports use `golded_ftn_jam`.
|
|
25
|
+
The source is public on GitHub. This package has not been released on PyPI.
|
|
26
|
+
|
|
27
|
+
Read and edit JAM revision 1 areas through the `golded-ftn` models. Python 3.12+.
|
|
28
|
+
Version 1.2.0 is prepared locally; these writer changes are unreleased.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
git clone https://github.com/golded-dev/golded-ftn-python.git
|
|
32
|
+
git clone https://github.com/golded-dev/golded-ftn-jam-python.git
|
|
33
|
+
cd golded-ftn-jam-python
|
|
34
|
+
uv sync --locked
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from tempfile import TemporaryDirectory
|
|
40
|
+
|
|
41
|
+
from golded_ftn import MessageBaseReader, ReaderOptions
|
|
42
|
+
from golded_ftn_jam import JamReader
|
|
43
|
+
|
|
44
|
+
# A minimal empty area. Real callers pass the area basename, without an extension.
|
|
45
|
+
with TemporaryDirectory() as directory:
|
|
46
|
+
base = Path(directory) / "example"
|
|
47
|
+
header = bytearray(1024)
|
|
48
|
+
header[:4] = b"JAM\0"
|
|
49
|
+
header[20:24] = (1).to_bytes(4, "little")
|
|
50
|
+
base.with_suffix(".JHR").write_bytes(header)
|
|
51
|
+
base.with_suffix(".JDT").write_bytes(b"")
|
|
52
|
+
base.with_suffix(".JDX").write_bytes(b"")
|
|
53
|
+
reader: MessageBaseReader = JamReader()
|
|
54
|
+
messages = list(reader.read(base, ReaderOptions(fallback_charset="CP850")))
|
|
55
|
+
assert messages == []
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The basename is exact; `.JHR`, `.JDT` and `.JDX` extensions are matched without
|
|
59
|
+
regard to case. Each must identify one regular file; symlinks and ambiguous
|
|
60
|
+
extension variants are rejected. The standalone reader ignores `.JLR`; editing
|
|
61
|
+
preserves it and creation initializes an empty file.
|
|
62
|
+
|
|
63
|
+
The index determines message numbers and which headers are live. Index holes and
|
|
64
|
+
deleted messages are skipped. Old unindexed headers are ignored. The reader
|
|
65
|
+
validates signatures, revision, record boundaries, offsets, subfield lengths,
|
|
66
|
+
message numbers and reused header offsets. It reads the whole area into memory
|
|
67
|
+
and validates all indexed records before returning a tuple. A malformed later
|
|
68
|
+
record cannot leave a caller with a partial result. Filesystem errors remain
|
|
69
|
+
filesystem errors; damaged data and decoding errors raise `ParserException` with
|
|
70
|
+
source path, byte offset and a chained cause.
|
|
71
|
+
|
|
72
|
+
The standalone `JamReader` requires a stable area with no concurrent writes. It
|
|
73
|
+
does not lock the area or produce a snapshot across the three files. Stop the
|
|
74
|
+
writer or use a consistent copy before reading.
|
|
75
|
+
|
|
76
|
+
Decoding is strict, with core charset detection and CP850 fallback. Header
|
|
77
|
+
FTSKLUDGE declarations are considered before body declarations. Conflicting
|
|
78
|
+
charset declarations and IDs fail. Unknown charset names use the configured
|
|
79
|
+
fallback. Mojibake repair is the caller's choice.
|
|
80
|
+
|
|
81
|
+
`body_text` contains only normalized `.JDT` text. Header control and routing
|
|
82
|
+
subfields go into `control_lines`, ahead of body metadata. Repeated controls and
|
|
83
|
+
routing entries keep their order within the core model's kludges, seen_by and
|
|
84
|
+
path sequences. Unknown subfields and nonzero HiID fields are bounds-checked
|
|
85
|
+
and skipped. The first originating/destination address is preserved as decoded
|
|
86
|
+
text, including node 0, point and domain. Other single-value subfields must agree
|
|
87
|
+
when repeated. Header MSGID/REPLYID take precedence over matching body IDs;
|
|
88
|
+
missing MSGID uses the core synthetic ID over decoded, normalized fields.
|
|
89
|
+
|
|
90
|
+
Dates are naive `1970-01-01 + DateWritten seconds`, independent of the machine's
|
|
91
|
+
timezone; zero means `None`. TZUTC is retained as a control line. Raw attributes,
|
|
92
|
+
all three reply links and `.JHR` provenance are retained; zero reply links become
|
|
93
|
+
`None`. Unknown area metadata stays `None`.
|
|
94
|
+
|
|
95
|
+
Compressed, encrypted and escaped active messages are unsupported. Area
|
|
96
|
+
discovery, databases, packing and repair are outside this package.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
uv sync --locked --python 3.14
|
|
102
|
+
uv run pytest
|
|
103
|
+
uv run ruff check .
|
|
104
|
+
uv run ruff format --check .
|
|
105
|
+
uv run mypy
|
|
106
|
+
uv run python -m mypy.stubtest golded_ftn_jam
|
|
107
|
+
uv build
|
|
108
|
+
uv run twine check dist/*
|
|
109
|
+
uv run python scripts/verify_distribution.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
uv uses the sibling `../golded-ftn-python` checkout during development. Published metadata
|
|
113
|
+
contains only `golded-ftn>=1.2.0,<2`. The sdist build hook removes `tool.uv.sources`
|
|
114
|
+
from the packed `pyproject.toml`; the development lock is also excluded.
|
|
115
|
+
Unpacked sources use the public dependency constraint.
|
|
116
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [release checks](docs/release.md).
|
|
117
|
+
|
|
118
|
+
## Format references and credits
|
|
119
|
+
|
|
120
|
+
- [JAM-001 revision 1](https://raw.githubusercontent.com/Mithgol/node-fidonet-jam/master/JAM.txt)
|
|
121
|
+
- [GoldED JAM structures](https://github.com/golded-dev/golded-linux-macos/blob/main/goldlib/gmb3/gmojamm.h)
|
|
122
|
+
- The sibling `laravel-ftn-jam/src/JamReader.php` and its reader tests informed
|
|
123
|
+
model mapping. Its sequential scan and permissive truncation are not used.
|
|
124
|
+
|
|
125
|
+
JAM(mbp) - Copyright 1993 Joaquim Homrighausen, Andrew Milner, Mats Birch, Mats Wallin. ALL RIGHTS RESERVED.
|
|
126
|
+
|
|
127
|
+
The package code is MIT licensed; the JAM specification has its own terms.
|
|
128
|
+
|
|
129
|
+
## Archive mode
|
|
130
|
+
|
|
131
|
+
Strict reading remains the default. Archive mode requires a report callback:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from golded_ftn import ReaderIssue, ReaderOptions
|
|
135
|
+
|
|
136
|
+
issues: list[ReaderIssue] = []
|
|
137
|
+
options = ReaderOptions(archive_mode=True, on_issue=issues.append)
|
|
138
|
+
# Pass options to JamReader().read(source, options).
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Issues carry `recovered`, `skipped` or `stopped`, the actual filename, record
|
|
142
|
+
identity and physical offset. Their detail contains no message contents. A stop
|
|
143
|
+
means the traversal is incomplete; a validated prefix may still be returned.
|
|
144
|
+
Multiple issues can describe one record, including recovery followed by a skip.
|
|
145
|
+
Filesystem errors and callback exceptions propagate. Files must remain stable.
|
|
146
|
+
|
|
147
|
+
Bounded fields exceeding JAM's specification limits are retained and reported.
|
|
148
|
+
Malformed TZUTC metadata is retained without interpretation. Distinct PID, FLAGS
|
|
149
|
+
and TZUTC subfields stay in source order. Conflicting names, subjects, IDs or
|
|
150
|
+
charset declarations are skipped rather than guessed. Failed records are skipped
|
|
151
|
+
using the next fixed index slot; reused header offsets stop traversal.
|
|
152
|
+
|
|
153
|
+
If declared ASCII cannot decode a payload, the configured fallback is tried
|
|
154
|
+
strictly and reported. The original charset control stays unchanged. Other
|
|
155
|
+
decoding failures are skipped; there is no lossy decoding or mojibake repair.
|
|
156
|
+
|
|
157
|
+
## Writing
|
|
158
|
+
|
|
159
|
+
`JamWriter.create(base)` creates `.JHR`, `.JDT`, `.JDX` and an empty `.JLR`.
|
|
160
|
+
Existing files are refused. The initial message number is 1.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from golded_ftn import MessagePatch, OutgoingMessage
|
|
164
|
+
from golded_ftn_jam import JamWriter
|
|
165
|
+
|
|
166
|
+
writer = JamWriter()
|
|
167
|
+
writer.create("new-area")
|
|
168
|
+
with writer.open("new-area") as session:
|
|
169
|
+
result = session.append(
|
|
170
|
+
OutgoingMessage(
|
|
171
|
+
from_name="Alice",
|
|
172
|
+
to_name="Bob",
|
|
173
|
+
subject="Hello",
|
|
174
|
+
body_text="Hello Bob",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
result = session.update(
|
|
178
|
+
result.identity, MessagePatch(subject="Changed"), result.revision
|
|
179
|
+
)
|
|
180
|
+
session.delete(result.identity, result.revision)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Every operation rereads and validates the index, referenced headers, text ranges
|
|
184
|
+
and active-message count under the byte-0 `.JHR` record lock. A session read returns
|
|
185
|
+
`SessionMessage` with a raw-byte SHA-256 revision. Changes to other messages do not
|
|
186
|
+
invalidate it. The revision includes identity, physical index/header/text locations
|
|
187
|
+
and SHA-256 over raw header, subfield and text bytes. An omitted patch field
|
|
188
|
+
retains its raw metadata; explicit `None` clears optional metadata. Names, subject, text and attributes reject `None`.
|
|
189
|
+
|
|
190
|
+
`control_lines` replaces general controls in both header subfields and inline
|
|
191
|
+
text. MSGID, address controls and routing retain their separate fields when
|
|
192
|
+
omitted; conflicting explicit controls are rejected. `external_id` and routing
|
|
193
|
+
patches remove obsolete inline copies as well as replacing their header metadata.
|
|
194
|
+
Body-only changes retain existing inline controls and routing. Unknown subfields
|
|
195
|
+
and nonzero HiID values survive unless their supported field is explicitly changed.
|
|
196
|
+
|
|
197
|
+
Content updates append a header, subfields and text, then redirect the index.
|
|
198
|
+
Header-only changes retain the text bytes and record position. Unknown subfields,
|
|
199
|
+
reserved words, timestamps and reply links remain intact. Delete marks the header,
|
|
200
|
+
sets the recipient index CRC to FFFFFFFF, and decrements the active count. Message
|
|
201
|
+
numbers and lastread data remain unchanged.
|
|
202
|
+
|
|
203
|
+
CP850 is the default. Encoding is strict; conflicting charset declarations,
|
|
204
|
+
truncation, unsupported compression flags and arbitrary reply lists are refused.
|
|
205
|
+
No MSGID, routing or duplicate detection is generated. Full-file snapshots support
|
|
206
|
+
in-place rollback during ordinary I/O failures; rollback failure poisons the session.
|
|
207
|
+
This provides no process-kill or power-loss transaction guarantee.
|
|
208
|
+
|
|
209
|
+
GoldED coexistence is disabled on every platform (`concurrent=True` is refused).
|
|
210
|
+
macOS/Linux use POSIX record locking. Core provides Windows offline record locks
|
|
211
|
+
and I/O, but this checkout has only been tested on macOS. Windows and Linux
|
|
212
|
+
execution and GoldED interoperability remain unverified. Keep GoldED closed and
|
|
213
|
+
avoid direct file access while these sessions operate. GoldED builds and
|
|
214
|
+
integration tests are deferred.
|
|
215
|
+
|
|
216
|
+
### Original-source evidence
|
|
217
|
+
|
|
218
|
+
The reference checkout is `golded-open-source`, commit
|
|
219
|
+
`600266252b73174ff5116cee697ef9a97aeb1859`. It contains
|
|
220
|
+
`goldlib/gmb3/gmojamm.h` (`JamHdrInfo`,
|
|
221
|
+
`JamHdr`, `JamIndex`, subfield IDs), `gmojamm2.cpp` (`open_area` initialization,
|
|
222
|
+
`scan` indexing), and `gmojamm4.cpp` (`lock`, `unlock`, `save_message`).
|
|
223
|
+
`lock` takes byte 0 length 1 in `.JHR`; `save_message` maintains recipient/MSGID/
|
|
224
|
+
REPLY CRCs, active count and modification counter. Its CRC seed and missing final
|
|
225
|
+
complement are confirmed by `goldlib/gall/gcrcs32.cpp::strCrc32`.
|
|
226
|
+
These source checks establish layout and write semantics. They do not establish
|
|
227
|
+
that a running GoldED reader refreshes safely after external changes.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# golded-ftn-jam
|
|
2
|
+
|
|
3
|
+
Repository: [`golded-ftn-jam-python`](https://github.com/golded-dev/golded-ftn-jam-python).
|
|
4
|
+
The distribution remains `golded-ftn-jam`; imports use `golded_ftn_jam`.
|
|
5
|
+
The source is public on GitHub. This package has not been released on PyPI.
|
|
6
|
+
|
|
7
|
+
Read and edit JAM revision 1 areas through the `golded-ftn` models. Python 3.12+.
|
|
8
|
+
Version 1.2.0 is prepared locally; these writer changes are unreleased.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
git clone https://github.com/golded-dev/golded-ftn-python.git
|
|
12
|
+
git clone https://github.com/golded-dev/golded-ftn-jam-python.git
|
|
13
|
+
cd golded-ftn-jam-python
|
|
14
|
+
uv sync --locked
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from tempfile import TemporaryDirectory
|
|
20
|
+
|
|
21
|
+
from golded_ftn import MessageBaseReader, ReaderOptions
|
|
22
|
+
from golded_ftn_jam import JamReader
|
|
23
|
+
|
|
24
|
+
# A minimal empty area. Real callers pass the area basename, without an extension.
|
|
25
|
+
with TemporaryDirectory() as directory:
|
|
26
|
+
base = Path(directory) / "example"
|
|
27
|
+
header = bytearray(1024)
|
|
28
|
+
header[:4] = b"JAM\0"
|
|
29
|
+
header[20:24] = (1).to_bytes(4, "little")
|
|
30
|
+
base.with_suffix(".JHR").write_bytes(header)
|
|
31
|
+
base.with_suffix(".JDT").write_bytes(b"")
|
|
32
|
+
base.with_suffix(".JDX").write_bytes(b"")
|
|
33
|
+
reader: MessageBaseReader = JamReader()
|
|
34
|
+
messages = list(reader.read(base, ReaderOptions(fallback_charset="CP850")))
|
|
35
|
+
assert messages == []
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The basename is exact; `.JHR`, `.JDT` and `.JDX` extensions are matched without
|
|
39
|
+
regard to case. Each must identify one regular file; symlinks and ambiguous
|
|
40
|
+
extension variants are rejected. The standalone reader ignores `.JLR`; editing
|
|
41
|
+
preserves it and creation initializes an empty file.
|
|
42
|
+
|
|
43
|
+
The index determines message numbers and which headers are live. Index holes and
|
|
44
|
+
deleted messages are skipped. Old unindexed headers are ignored. The reader
|
|
45
|
+
validates signatures, revision, record boundaries, offsets, subfield lengths,
|
|
46
|
+
message numbers and reused header offsets. It reads the whole area into memory
|
|
47
|
+
and validates all indexed records before returning a tuple. A malformed later
|
|
48
|
+
record cannot leave a caller with a partial result. Filesystem errors remain
|
|
49
|
+
filesystem errors; damaged data and decoding errors raise `ParserException` with
|
|
50
|
+
source path, byte offset and a chained cause.
|
|
51
|
+
|
|
52
|
+
The standalone `JamReader` requires a stable area with no concurrent writes. It
|
|
53
|
+
does not lock the area or produce a snapshot across the three files. Stop the
|
|
54
|
+
writer or use a consistent copy before reading.
|
|
55
|
+
|
|
56
|
+
Decoding is strict, with core charset detection and CP850 fallback. Header
|
|
57
|
+
FTSKLUDGE declarations are considered before body declarations. Conflicting
|
|
58
|
+
charset declarations and IDs fail. Unknown charset names use the configured
|
|
59
|
+
fallback. Mojibake repair is the caller's choice.
|
|
60
|
+
|
|
61
|
+
`body_text` contains only normalized `.JDT` text. Header control and routing
|
|
62
|
+
subfields go into `control_lines`, ahead of body metadata. Repeated controls and
|
|
63
|
+
routing entries keep their order within the core model's kludges, seen_by and
|
|
64
|
+
path sequences. Unknown subfields and nonzero HiID fields are bounds-checked
|
|
65
|
+
and skipped. The first originating/destination address is preserved as decoded
|
|
66
|
+
text, including node 0, point and domain. Other single-value subfields must agree
|
|
67
|
+
when repeated. Header MSGID/REPLYID take precedence over matching body IDs;
|
|
68
|
+
missing MSGID uses the core synthetic ID over decoded, normalized fields.
|
|
69
|
+
|
|
70
|
+
Dates are naive `1970-01-01 + DateWritten seconds`, independent of the machine's
|
|
71
|
+
timezone; zero means `None`. TZUTC is retained as a control line. Raw attributes,
|
|
72
|
+
all three reply links and `.JHR` provenance are retained; zero reply links become
|
|
73
|
+
`None`. Unknown area metadata stays `None`.
|
|
74
|
+
|
|
75
|
+
Compressed, encrypted and escaped active messages are unsupported. Area
|
|
76
|
+
discovery, databases, packing and repair are outside this package.
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
uv sync --locked --python 3.14
|
|
82
|
+
uv run pytest
|
|
83
|
+
uv run ruff check .
|
|
84
|
+
uv run ruff format --check .
|
|
85
|
+
uv run mypy
|
|
86
|
+
uv run python -m mypy.stubtest golded_ftn_jam
|
|
87
|
+
uv build
|
|
88
|
+
uv run twine check dist/*
|
|
89
|
+
uv run python scripts/verify_distribution.py
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
uv uses the sibling `../golded-ftn-python` checkout during development. Published metadata
|
|
93
|
+
contains only `golded-ftn>=1.2.0,<2`. The sdist build hook removes `tool.uv.sources`
|
|
94
|
+
from the packed `pyproject.toml`; the development lock is also excluded.
|
|
95
|
+
Unpacked sources use the public dependency constraint.
|
|
96
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [release checks](docs/release.md).
|
|
97
|
+
|
|
98
|
+
## Format references and credits
|
|
99
|
+
|
|
100
|
+
- [JAM-001 revision 1](https://raw.githubusercontent.com/Mithgol/node-fidonet-jam/master/JAM.txt)
|
|
101
|
+
- [GoldED JAM structures](https://github.com/golded-dev/golded-linux-macos/blob/main/goldlib/gmb3/gmojamm.h)
|
|
102
|
+
- The sibling `laravel-ftn-jam/src/JamReader.php` and its reader tests informed
|
|
103
|
+
model mapping. Its sequential scan and permissive truncation are not used.
|
|
104
|
+
|
|
105
|
+
JAM(mbp) - Copyright 1993 Joaquim Homrighausen, Andrew Milner, Mats Birch, Mats Wallin. ALL RIGHTS RESERVED.
|
|
106
|
+
|
|
107
|
+
The package code is MIT licensed; the JAM specification has its own terms.
|
|
108
|
+
|
|
109
|
+
## Archive mode
|
|
110
|
+
|
|
111
|
+
Strict reading remains the default. Archive mode requires a report callback:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from golded_ftn import ReaderIssue, ReaderOptions
|
|
115
|
+
|
|
116
|
+
issues: list[ReaderIssue] = []
|
|
117
|
+
options = ReaderOptions(archive_mode=True, on_issue=issues.append)
|
|
118
|
+
# Pass options to JamReader().read(source, options).
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Issues carry `recovered`, `skipped` or `stopped`, the actual filename, record
|
|
122
|
+
identity and physical offset. Their detail contains no message contents. A stop
|
|
123
|
+
means the traversal is incomplete; a validated prefix may still be returned.
|
|
124
|
+
Multiple issues can describe one record, including recovery followed by a skip.
|
|
125
|
+
Filesystem errors and callback exceptions propagate. Files must remain stable.
|
|
126
|
+
|
|
127
|
+
Bounded fields exceeding JAM's specification limits are retained and reported.
|
|
128
|
+
Malformed TZUTC metadata is retained without interpretation. Distinct PID, FLAGS
|
|
129
|
+
and TZUTC subfields stay in source order. Conflicting names, subjects, IDs or
|
|
130
|
+
charset declarations are skipped rather than guessed. Failed records are skipped
|
|
131
|
+
using the next fixed index slot; reused header offsets stop traversal.
|
|
132
|
+
|
|
133
|
+
If declared ASCII cannot decode a payload, the configured fallback is tried
|
|
134
|
+
strictly and reported. The original charset control stays unchanged. Other
|
|
135
|
+
decoding failures are skipped; there is no lossy decoding or mojibake repair.
|
|
136
|
+
|
|
137
|
+
## Writing
|
|
138
|
+
|
|
139
|
+
`JamWriter.create(base)` creates `.JHR`, `.JDT`, `.JDX` and an empty `.JLR`.
|
|
140
|
+
Existing files are refused. The initial message number is 1.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from golded_ftn import MessagePatch, OutgoingMessage
|
|
144
|
+
from golded_ftn_jam import JamWriter
|
|
145
|
+
|
|
146
|
+
writer = JamWriter()
|
|
147
|
+
writer.create("new-area")
|
|
148
|
+
with writer.open("new-area") as session:
|
|
149
|
+
result = session.append(
|
|
150
|
+
OutgoingMessage(
|
|
151
|
+
from_name="Alice",
|
|
152
|
+
to_name="Bob",
|
|
153
|
+
subject="Hello",
|
|
154
|
+
body_text="Hello Bob",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
result = session.update(
|
|
158
|
+
result.identity, MessagePatch(subject="Changed"), result.revision
|
|
159
|
+
)
|
|
160
|
+
session.delete(result.identity, result.revision)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Every operation rereads and validates the index, referenced headers, text ranges
|
|
164
|
+
and active-message count under the byte-0 `.JHR` record lock. A session read returns
|
|
165
|
+
`SessionMessage` with a raw-byte SHA-256 revision. Changes to other messages do not
|
|
166
|
+
invalidate it. The revision includes identity, physical index/header/text locations
|
|
167
|
+
and SHA-256 over raw header, subfield and text bytes. An omitted patch field
|
|
168
|
+
retains its raw metadata; explicit `None` clears optional metadata. Names, subject, text and attributes reject `None`.
|
|
169
|
+
|
|
170
|
+
`control_lines` replaces general controls in both header subfields and inline
|
|
171
|
+
text. MSGID, address controls and routing retain their separate fields when
|
|
172
|
+
omitted; conflicting explicit controls are rejected. `external_id` and routing
|
|
173
|
+
patches remove obsolete inline copies as well as replacing their header metadata.
|
|
174
|
+
Body-only changes retain existing inline controls and routing. Unknown subfields
|
|
175
|
+
and nonzero HiID values survive unless their supported field is explicitly changed.
|
|
176
|
+
|
|
177
|
+
Content updates append a header, subfields and text, then redirect the index.
|
|
178
|
+
Header-only changes retain the text bytes and record position. Unknown subfields,
|
|
179
|
+
reserved words, timestamps and reply links remain intact. Delete marks the header,
|
|
180
|
+
sets the recipient index CRC to FFFFFFFF, and decrements the active count. Message
|
|
181
|
+
numbers and lastread data remain unchanged.
|
|
182
|
+
|
|
183
|
+
CP850 is the default. Encoding is strict; conflicting charset declarations,
|
|
184
|
+
truncation, unsupported compression flags and arbitrary reply lists are refused.
|
|
185
|
+
No MSGID, routing or duplicate detection is generated. Full-file snapshots support
|
|
186
|
+
in-place rollback during ordinary I/O failures; rollback failure poisons the session.
|
|
187
|
+
This provides no process-kill or power-loss transaction guarantee.
|
|
188
|
+
|
|
189
|
+
GoldED coexistence is disabled on every platform (`concurrent=True` is refused).
|
|
190
|
+
macOS/Linux use POSIX record locking. Core provides Windows offline record locks
|
|
191
|
+
and I/O, but this checkout has only been tested on macOS. Windows and Linux
|
|
192
|
+
execution and GoldED interoperability remain unverified. Keep GoldED closed and
|
|
193
|
+
avoid direct file access while these sessions operate. GoldED builds and
|
|
194
|
+
integration tests are deferred.
|
|
195
|
+
|
|
196
|
+
### Original-source evidence
|
|
197
|
+
|
|
198
|
+
The reference checkout is `golded-open-source`, commit
|
|
199
|
+
`600266252b73174ff5116cee697ef9a97aeb1859`. It contains
|
|
200
|
+
`goldlib/gmb3/gmojamm.h` (`JamHdrInfo`,
|
|
201
|
+
`JamHdr`, `JamIndex`, subfield IDs), `gmojamm2.cpp` (`open_area` initialization,
|
|
202
|
+
`scan` indexing), and `gmojamm4.cpp` (`lock`, `unlock`, `save_message`).
|
|
203
|
+
`lock` takes byte 0 length 1 in `.JHR`; `save_message` maintains recipient/MSGID/
|
|
204
|
+
REPLY CRCs, active count and modification counter. Its CRC seed and missing final
|
|
205
|
+
complement are confirmed by `goldlib/gall/gcrcs32.cpp::strCrc32`.
|
|
206
|
+
These source checks establish layout and write semantics. They do not establish
|
|
207
|
+
that a running GoldED reader refreshes safely after external changes.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Treat message archives as untrusted input. Readers validate record boundaries,
|
|
4
|
+
offsets and decoding, but archives must remain stable while being read.
|
|
5
|
+
|
|
6
|
+
Report vulnerabilities privately through [GitHub security advisories](https://github.com/golded-dev/golded-ftn-jam-python/security/advisories/new).
|
|
7
|
+
Do not put private archives, message contents or credentials in public issues.
|
|
8
|
+
Security review targets the current 1.2.x development line. The writer changes
|
|
9
|
+
are prepared locally and have not been published on PyPI.
|
|
10
|
+
|
|
11
|
+
Writer sessions validate the base under their operation lock and roll back
|
|
12
|
+
handled I/O failures. Keep GoldED closed; direct file access bypasses this lock.
|
|
13
|
+
No recovery guarantee covers process termination or power loss. Archive mode is
|
|
14
|
+
for reading damaged records, never for editing them.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Release checks
|
|
2
|
+
|
|
3
|
+
Run every README development check from a clean checkout with sibling core.
|
|
4
|
+
`scripts/verify_distribution.py` inspects wheel/sdist contents and metadata,
|
|
5
|
+
rebuilds the wheel from sdist, compares archive bytes, builds a local core wheel,
|
|
6
|
+
and tests both JAM wheels in separate environments outside the repositories.
|
|
7
|
+
It also checks runtime imports, README execution, strict test typing and stubtest.
|
|
8
|
+
|
|
9
|
+
CI runs full checks on Linux Python 3.12, 3.13 and 3.14, with pytest on Windows
|
|
10
|
+
and macOS Python 3.14. Local checks do not establish that CI passed.
|
|
11
|
+
|
|
12
|
+
Inspect CHANGELOG and license notices. Commit only when requested. A local
|
|
13
|
+
1.2.0 build does not publish a release. Remote creation, push, tagging and package
|
|
14
|
+
publication each require explicit authorization.
|
|
15
|
+
|
|
16
|
+
For writer changes, check create/read/append/update/delete through public sessions,
|
|
17
|
+
independent binary fixtures, stale revisions, controlled lock contention and
|
|
18
|
+
handled write/flush failures. Run examples against the installed wheel. Record
|
|
19
|
+
platform results separately: local macOS tests do not establish Linux or Windows
|
|
20
|
+
execution, or GoldED compatibility. Keep `concurrent=True` disabled until both
|
|
21
|
+
competing writes and GoldED read/cache/refresh checks pass against a pinned build.
|
|
22
|
+
The current GoldED build and integration tests are deferred.
|
|
23
|
+
|
|
24
|
+
## Local 1.2.0 release candidate — 2026-10-05
|
|
25
|
+
|
|
26
|
+
Verified on macOS 27.0 arm64 with CPython 3.14.6. The checkout contains
|
|
27
|
+
uncommitted changes; these checks cover the working tree, not a tagged release.
|
|
28
|
+
|
|
29
|
+
- `uv sync --locked`: passed.
|
|
30
|
+
- Ruff lint and format checks, strict mypy: passed.
|
|
31
|
+
- `uv run pytest -q`: 127 passed, 1 skipped.
|
|
32
|
+
- `uv build` and `uv run twine check dist/*`: passed for wheel and sdist.
|
|
33
|
+
- `scripts/verify_distribution.py`: passed metadata and package-content checks,
|
|
34
|
+
byte comparison against a wheel rebuilt from sdist, isolated installed-package
|
|
35
|
+
tests and strict consumer typing. Format packages also pass installed stubtest.
|
|
36
|
+
- `agent-compose check`: passed using the local mostly-agents tool.
|
|
37
|
+
- `git diff --check`: passed (whitespace only).
|
|
38
|
+
|
|
39
|
+
GitHub's API reports the repository as public and private vulnerability reporting
|
|
40
|
+
as enabled. PyPI's project JSON endpoint returned HTTP 404 on this date. No package
|
|
41
|
+
was uploaded. Local checks do not establish Linux/Windows or remote CI results.
|
|
42
|
+
GoldED build interoperability remains deferred; concurrent use stays disabled.
|
|
43
|
+
|
|
44
|
+
Release order: publish `golded-ftn==1.2.0` first, then the four format packages.
|
|
45
|
+
Each format package requires `golded-ftn>=1.2.0,<2`. Before publication, commit
|
|
46
|
+
and review CI for these exact sources, create the intended release tag, and
|
|
47
|
+
confirm the package-index destination and publishing authority. After core is
|
|
48
|
+
available, verify resolution from that index without local uv sources. Publish
|
|
49
|
+
only the reviewed archives, then check public installation and update the shared
|
|
50
|
+
guide's commit pins to the released commits. These remote actions are not part
|
|
51
|
+
of this local preparation.
|
|
52
|
+
|
|
53
|
+
Archive checksums are recorded separately in `RELEASE-SHA256.txt` at the
|
|
54
|
+
repository root, outside the archives, after the final build.
|
|
55
|
+
|
|
56
|
+
## PyPI Trusted Publishing
|
|
57
|
+
|
|
58
|
+
Create a PyPI account, verify its email and configure two-factor authentication.
|
|
59
|
+
For a first publication, add a pending publisher at
|
|
60
|
+
<https://pypi.org/manage/account/publishing/> with these exact fields:
|
|
61
|
+
|
|
62
|
+
- PyPI project: `golded-ftn-jam`
|
|
63
|
+
- GitHub owner: `golded-dev`
|
|
64
|
+
- GitHub repository: `golded-ftn-jam-python`
|
|
65
|
+
- Workflow filename: `publish.yml`
|
|
66
|
+
- Environment: `pypi`
|
|
67
|
+
|
|
68
|
+
See [PyPI's pending-publisher instructions](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/).
|
|
69
|
+
No API token or password is required by the workflow. The account setup is a
|
|
70
|
+
manual prerequisite; a GitHub release does not create the PyPI project.
|
|
71
|
+
|
|
72
|
+
After this tag's CI succeeds and the GitHub release contains both archives and
|
|
73
|
+
`RELEASE-SHA256.txt`, run:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
gh workflow run publish.yml --repo golded-dev/golded-ftn-jam-python -f tag=v1.2.0
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The workflow verifies SHA-256 and uploads those exact release assets. Publish
|
|
80
|
+
core first, verify installation from PyPI, then dispatch the format workflows.
|
|
81
|
+
Confirm the workflow result, PyPI version and hashes, and installation in a fresh
|
|
82
|
+
environment. Do not store publishing credentials in this repository.
|
|
83
|
+
|
|
84
|
+
The deterministic process-lock helper uses native `msvcrt` byte locks on
|
|
85
|
+
Windows and `fcntl` record locks on POSIX. Both coordinate through an explicit
|
|
86
|
+
ready signal and release request; the test does not depend on random timing.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "golded-ftn-jam"
|
|
7
|
+
version = "1.2.0"
|
|
8
|
+
description = "Strict reader and offline writer for JAM revision 1 message areas"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "GoldED.dev contributors"}]
|
|
14
|
+
dependencies = ["golded-ftn>=1.2.0,<2"]
|
|
15
|
+
classifiers = ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Typing :: Typed", "Operating System :: OS Independent"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Repository = "https://github.com/golded-dev/golded-ftn-jam-python"
|
|
19
|
+
Issues = "https://github.com/golded-dev/golded-ftn-jam-python/issues"
|
|
20
|
+
Changelog = "https://github.com/golded-dev/golded-ftn-jam-python/blob/main/CHANGELOG.md"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = ["pytest>=8", "ruff>=0.11", "mypy>=1.15", "build>=1.2", "twine>=6", "hatchling>=1.26"]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/golded_ftn_jam"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.sdist]
|
|
29
|
+
include = ["src", "tests", "scripts", "docs", "README.md", "CHANGELOG.md", "CONTRIBUTING.md", "SECURITY.md", "LICENSE", "pyproject.toml"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.sdist.hooks.custom]
|
|
32
|
+
path = "scripts/sdist_hook.py"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
35
|
+
target-version = "py312"
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
strict = true
|
|
42
|
+
files = ["src", "tests", "scripts"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|