Copying files onto a SINTRAN III pack¶
How to get files from Windows onto a SINTRAN III disk image, without silently corrupting them. Written while installing COSMOS TCP/IP, but nothing here is TCP/IP-specific.
Everything below was measured on 2026-08-21 unless marked otherwise. Where a claim is not verified it says so.
Tooling: the ndfs-py library and the ndtool CLI both come from
norskdata-ndfs — https://github.com/HackerCorpLabs/norskdata-ndfs.
Take ndtool from the releases page: https://github.com/HackerCorpLabs/norskdata-ndfs/releases
1. Pick a route¶
| Route | Use it when | Safe for writes? |
|---|---|---|
A — ndfs-py |
the normal case: bulk copy into a .img |
Yes (0.0.6) |
B — ndtool.exe |
listing, extracting, --fsck |
Yes (0.0.6) — see §5 |
| C — floppy image | you want the machine to do the copy itself | Yes |
D — COPY-FILE ,TERM |
one short text file, no tooling | Yes, but tedious |
Route A is what the rest of this document assumes. It is the only one that restores the original file metadata (type, access bits, dates, sparse holes) in one step.
Two conditions apply to A and B both:
- Stop the emulator first. A running RetroCore holds the image. Writing to a pack it
has open is not safe; if you are lucky the write fails with
PermissionError, which is the good outcome. Stop the machine, back up, write, restart. - Work on a copy. These packs are irreplaceable.
2. The parity problem — read this before you write anything¶
SINTRAN stores text files with ND-100 even parity: bit 7 of each byte is set only
when the low seven bits contain an odd number of ones. Binary files (:PROG, :BPUN,
:BRF, :DATA) are raw and must never be touched.
Get this wrong in either direction and the failure is quiet:
| Mistake | What you see |
|---|---|
| Parity missing where it is needed | SINTRAN displays the file correctly on a terminal, so it looks fine — but DMAC and any program reading it hit ILL. CHARACTER on nearly every byte. A 4,800-line patch mode file once produced 2,812 ILL. CHARACTER errors and did nothing. |
| Parity applied to a binary | The program is destroyed. ndtool -x -p over a whole pack corrupts every :PROG on it. |
You cannot decide parity from the file type. It is per-file, per-pack.¶
This is the part that catches people. Measured across the two source packs for this product:
| File | c3 pack | tingo pack |
|---|---|---|
AIP-CONFIG:SYMB |
parity (69.2% high-bit) | — |
AIP-HOSTS:SYMB |
parity (62.4%) | parity (65.8%) |
AIP-NETWORKS:SYMB |
no parity (0.0%) | no parity (0.0%) |
AIP-PROTOCOL:SYMB |
no parity (0.0%) | no parity (0.0%) |
AIP-SERVICES:SYMB |
parity (72.3%) | no parity (0.0%) |
SKP-C00:DEFS |
no parity | — |
SKP-C00:IMPT |
parity (52.7%) | — |
SKP-C00:INTL |
no parity | — |
SLIB:DEFS / SLIB:IMPT |
no parity | — |
TCP-START-D02:MODE |
parity (76.6%) | — |
Two rows kill every shortcut anyone is tempted to invent:
SKP-C00is one base name with three types and two different answers. So "all:SYMBhave parity" or "all:DEFSdo not" are both wrong.AIP-SERVICES:SYMBhas parity on one pack and not on the other. So it is not even a property of the filename. It is a property of that file on that pack.
A genuine ND text file with parity has roughly 35–75% of its bytes with bit 7 set. A 7-bit file has 0.0%. That measurement is the check — not the file type.
The extracted kit in x/ is 7-bit¶
Every text file in x/ reads 0.0% high-bit: the extractor stripped parity, and in
doing so it normalised away the distinction in the table above. The sidecars do not
record parity either.
So there are two honest ways to restore:
- Byte-exact from the source pack (best — see §3b). Reproduces the original exactly and sidesteps the question.
- From
x/, re-applying parity only to the files the table above says had it (§3c). Correct for the files listed; for anything not listed, measure the original.
3. Route A — ndfs-py (recommended)¶
The ndfs-py library from norskdata-ndfs — https://github.com/HackerCorpLabs/norskdata-ndfs
Use 0.0.5 or later; 0.0.6 was what this was verified against (2026-08-21).
No build step: put its ndfs-py/src on PYTHONPATH.
$env:PYTHONPATH = '<ndfs-repo>\ndfs-py\src'
3a. Look before you write¶
from ndfs import NdfsFileSystem
fs = NdfsFileSystem(open(r'mypack.img','rb').read(), read_only=True)
print(fs.get_directory_name())
for u in fs.get_users():
print(u.name, u.reserved_pages)
Create the destination user first if it is missing — fs.add_user(name, reserved_pages).
For TCP/IP that is user TCP-IP with about a 1000-page quota.
3b. Byte-exact copy, pack → pack — the safest thing you can do¶
No transformation at all, so parity, holes and content are reproduced exactly.
src = NdfsFileSystem(open(SRC,'rb').read(), read_only=True)
dst = NdfsFileSystem(bytearray(open(DST,'rb').read()))
for e in src.get_object_entries():
if e.user_name != 'TCP-IP':
continue
path = f'{e.user_name}/{e.object_name}:{e.type}'
data, props = src.read_file_with_properties(path) # raw bytes + XAT metadata
dst.write_file_with_properties(path, data, props) # restores holes, access, dates
open(DST,'wb').write(dst.to_buffer())
write_file_with_properties is the one call that legitimately recreates sparse holes,
because the sidecar records where the original's holes were. That matters: a hole is a
property of the file, not of the data. Inferring holes from "this page is all zero"
produces a file SINTRAN cannot read — READ-BI and READ-PROGFILE fail on it with
NO SUCH PAGE / ERROR IN ACCESSING INPUT FILE.
It also restores access bits, so there is no separate "make it readable" step.
3c. Copy from the extracted kit in x/¶
Each file has a .xat sidecar carrying its real SINTRAN name, type, access bits, dates and
hole map. Use it — the Windows filename has lost the : (see §8).
import json, glob, os
from ndfs import NdfsFileSystem
# the files that carried parity on the c3 pack (measured - see §2)
NEEDS_PARITY = {
('SYSTEM','AIP-CONFIG','SYMB'), ('SYSTEM','AIP-HOSTS','SYMB'),
('SYSTEM','AIP-SERVICES','SYMB'),
('TCP-IP','DEFINE-FTPRT-D02','MODE'), ('TCP-IP','DEFINE-TCPP-D02','MODE'),
('TCP-IP','TCP-IP-LO-D02','MODE'), ('TCP-IP','TCP-IP-LO-D02','LIST'),
('TCP-IP','TCP-START-D02','MODE'), ('TCP-IP','TCP-START-D02','LIST'),
('TCP-IP','TCP-STOP-D02','MODE'),
('TCP-IP','TCP-IP-LO-C07','LIST'), ('TCP-IP','TCP-START-C07','LIST'),
('TCP-IP','SKP-C00','IMPT'),
}
dst = NdfsFileSystem(bytearray(open(DST,'rb').read()))
for xat_path in glob.glob(os.path.join(KIT, '**', '*.xat'), recursive=True):
props = json.load(open(xat_path))
user = props['ndfs.user_name']
name = props['ndfs.object_name']
typ = props['ndfs.type']
data = open(xat_path[:-4], 'rb').read() # strip ".xat"
if (user, name, typ) in NEEDS_PARITY:
data = bytes((b & 0x7F) | (0x80 if bin(b & 0x7F).count('1') % 2 else 0)
for b in data)
dst.write_file_with_properties(f'{user}/{name}:{typ}', data, props)
open(DST,'wb').write(dst.to_buffer())
parity values in this library are "none" and "set" — not "even". An older note
claimed write_file(path, data, "even") silently failed to set parity; the reason is that
"even" was never a valid value. Use "set", or encode it yourself as above.
The encoder is verified: it reproduces DEFINE-TCPP-D02:MODE, XMSG-START:MODE and
TCP-START-D02:MODE byte for byte. @ = 0x40 → 0xC0, C = 0x43 → 0xC3, - = 0x2D
unchanged.
Line endings: ND text is CR only — but not always. AIP-CONFIG:SYMB on c3 is
CRLF. Copy whatever the original had; do not normalise.
4. Route C — via a floppy image¶
Lets the machine do the copying, which means SINTRAN decides the on-disk form and the parity question does not arise.
- Build or obtain a floppy
.imgcontaining the files. - Attach it to the emulator's floppy controller.
- On the machine:
@ENTER-DIRECTORY ,,FLOPPY-DISC-1,0
@COPY-FILE "(PACK-ONE:TCP-IP)TCP-START-D02:MODE",(<label>:TCP-IP)TCP-START-D02:MODE
Quotes wrap the entire destination specification including the (USER) prefix —
"(USER)NAME:TYPE", never (USER)"NAME:TYPE". Quotes mean "create this file", so they go
on the destination.
Product installers usually expect this route, and some of them reopen themselves by bare
name against the default directory — if one aborts with "<name>:PROG" NO SUCH FILE NAME,
copy the installer onto SYSTEM and run it by bare name.
5. Route B — ndtool.exe¶
Get the latest release binary from https://github.com/HackerCorpLabs/norskdata-ndfs/releases — do not hunt for a build directory on disk. Verified here against 0.0.6 (2026-08-21).
Always run
-Vfirst. Versions before 0.0.5 read and write the allocation bitmap byte-swapped and can hand out pages SINTRAN is already using, overwriting live file data. Such a build is fine for reading and must never be used for--put,--rm,--create, or any user/quota/friend change.Do not pick a binary out of a local build directory. Two builds of different versions can sit side by side, and the stale one is easy to grab by accident — that is exactly how this document first got the version wrong. Take the release.
0.0.6 is past that bug by version and is verified here for reading (users, file listing,
against a real pack). A write has not been tested with it, so if you do write with
ndtool, work on a copy and --fsck afterwards. Route A remains the recommended write path.
$nd = '<path-to>\ndtool.exe' # from the releases page
& $nd -V # expect 0.0.6 or later
& $nd -i -v mypack.img # volume info + integrity
& $nd -u mypack.img # users and quotas
& $nd -t -u TCP-IP mypack.img # one user's files
& $nd --fsck mypack.img # 5-phase check
& $nd --stat 'TCP-IP/TCP-START-D02:MODE' mypack.img
# extract WITH sidecars - do this
& $nd -x --xat -o out\ mypack.img
# binaries: no -p
& $nd -x -d -l -o out\ mypack.img
# text only: -p strips parity
& $nd -x -p -F 'TCP-IP/*:MODE' -o out\ mypack.img
If -V reports below 0.0.5, download a current release (https://github.com/HackerCorpLabs/norskdata-ndfs/releases).
To build it yourself instead, from a checkout of the repo:
cmake -S <ndfs-repo>\ndfs-c -B <ndfs-repo>\ndfs-c\build -G "MinGW Makefiles"
cmake --build <ndfs-repo>\ndfs-c\build --target ndtool
& $nd -V # confirm it moved
Note that ndtool --put leaves PUBLIC access NONE, so it needs a follow-up
--chmod PUBLIC+R. Route A with a sidecar does not, because the access bits come from the
sidecar.
6. Route D — type it in at the terminal¶
For one short text file with no tooling at all:
@COPY-FILE "(TCP-IP)MYFILE:SYMB",TERM
…then type the content, terminated with CTRL-L. SINTRAN applies its own parity, so the result is a proper ND text file.
QED is the other option but is unreliable under RetroCore [open bug]: creating a new
file crashes with ERROR 24 AT 11452; ILLEGAL INSTRUCTION, reproducible at the same
address on K and L images, so it is emulator- or binary-side rather than an install fault.
Writing to an already existing file with an unquoted name works. Workaround:
CREATE-FILE first, then edit.
7. Verify the copy — do not skip this¶
1. Bank images carry their own checksum. :BPUN PIOC banks are the one ND file type
where "is this intact?" has a real answer:
python verify_pioc_bank.py mypack.img
All eight banks in x/ pass (verified after the copy into this repo, 2026-08-21):
TCP-SER-B0-D02 computed=2890 stored=2890 OK 74.4% content
TCP-SER-B1-D02 computed=5421 stored=5421 OK 30.1%
TCP-SER-B2-D02 computed=0000 stored=0000 OK 0.0% (empty bank)
TCP-SER-B3-D02 computed=99EE stored=99EE OK 2.3%
TCP-SER-B0-B05 computed=D998 stored=D998 OK 75.0%
TCP-SER-B1-B05 computed=25F3 stored=25F3 OK 12.1%
TCP-SER-B2-B05 computed=471E stored=471E OK 0.1%
TCP-SER-B3-B05 computed=42CE stored=42CE OK 9.2%
A parity mistake on a :BPUN shows up here instantly — which makes this the fastest test
that a bulk copy went in cleanly. Note TCP-SER-B2-D02 is 100% zero and correct: the
bank is simply unused. A PASS on a bank with real content is strong; a PASS on an empty one
proves little.
2. Check the parity of every text file you wrote, against §2:
d = fs.read_file('TCP-IP/TCP-START-D02:MODE', parity='none')
print(100.0*sum(1 for b in d if b & 0x80)/len(d)) # expect ~76%, not 0.0
3. --fsck the pack and compare free-page counts before and after.
4. Read a file back and compare it to the source byte for byte. For a pack-to-pack copy this should be exact.
Do not verify a text file by COPY-FILE-ing it to the terminal: SINTRAN ends lines
with CR and no LF, so every line prints at column 0 and overwrites the last. The file is
usually fine even though the screen looks like garbage. Use FILE-STATISTICS for the byte
count, or run it.
8. Traps that have already cost time¶
:cannot appear in a Windows filename. Extracted files use.instead —TCP-START-D02:MODEbecomesTCP-START-D02.MODE. The.xatsidecar carries the real name and type back. Always drive a restore from the sidecar, never from the Windows filename.- Same name, different type, overwritten. A pack legitimately carries
TCP-IP-LO-D02as both:MODEand:LIST, andSKP-C00as:DEFS,:IMPTand:INTL. Older library versions resolved a file by name only, so writingAAA:MODEafterAAA:LISToverwrote the LIST entry's data while keeping its type — the MODE file vanished and the LIST file held the wrong bytes. Fixed 2026-08-19 in ndfs-py and ndfs-ts; ndfs-c never had the bug. On an older checkout the symptom is: the copy reports N files written, the pack has fewer, and sizes belong to the wrong type. - An all-zero page is not a hole. See §3b — state holes from the sidecar, never infer them from the data.
- A file extracted from a damaged image looks fine and is not. Holes read as zeros, so
the size and header are right while the middle is NUL. Before trusting anything pulled
off a recovered pack, hash it against every other copy of that pack you have and check
the image with
img_sector_map.py. - Do not grep an image for a filename. Names live in object entries with a
0x27terminator and the pack may be sparse or fragmented. A grep that finds nothing proves nothing. Open the filesystem and enumerate. - A "missing" file may be a second object block. A user holds 256 files per object
block;
--objblocksshows whether more were granted. - PowerShell, not Bash.
cd e:\pathdoes not change drive on its own — use full paths.
9. Sources¶
| Library / tool | norskdata-ndfs — https://github.com/HackerCorpLabs/norskdata-ndfs · releases: https://github.com/HackerCorpLabs/norskdata-ndfs/releases. ndfs-py 0.0.6 and ndtool 0.0.6 verified 2026-08-21 |
| Format spec | docs/NDFS-FORMAT.md in the norskdata-ndfs repo |
| Parity map in §2 | measured from the 2024 read of the c3 Micropolis 1325 pack and the Tingo Micropolis 1325 pack, 2026-08-21 |
| Bank verifier | verify_pioc_bank.py (bank checksum layout documented in README.md §7) |
| Command syntax | ND-60.128.5 EN SINTRAN III Reference Manual |