What DiscMedic 0.10 does not do
===============================

This file is the honest list.  A disc checker that overstates what it
covers is worse than one that does less, because people stop copying their
data off when a checker says the disc is fine.


What it does cover
------------------

FAT12, FAT16 and FAT32, completely: the boot sector and, on FAT32, its
backup and the FSInfo sector; every entry in the file allocation table; the
second and any further copies of the table against the first; every
directory, long file name and short file name; every file's cluster chain
against its recorded length; clusters claimed twice; circular chains;
clusters that belong to nothing; and the clean-shutdown flags.

FileCore new-map discs, E/F and E+/F+, completely: the boot block's defect
lists and checksum; every field of the disc record; where the map is; the
check byte of every zone of both copies and the cross check that ties a
copy together; every fragment in the map and the free space chains; and
then the whole directory tree from the root - every directory's start and
end names, sequence numbers and check byte, every entry's name, attributes
and disc address, every object's length against the space allocated to it,
objects claimed by two directories, and objects with space allocated that
no directory mentions.


UFS2 (0.10)
-----------

Checked and repaired, by fsck_ffs's passes (FreeBSD sbin/fsck_ffs, read
for the rules; no code copied): the superblock against its first backup,
and replaced by a backup found by searching when the primary is gone (only
on a partition whose GPT type is FreeBSD UFS, or one named with -at); every
inode's type, size, check-hash, block pointers (range, alignment, metadata,
past the end), di_blocks; blocks held twice (every inode involved is
cleared, as fsck_ffs -y does); every directory's entry structure, ".", "..",
names, types, entries naming free or cleared inodes, extra links to a
directory; directories not reachable from the root and files nothing names,
to lost+found (made if missing, in the root, one block, "#<inode>" names);
link counts; and every cylinder group's inode map, free map, counts,
fragment summary, cluster map and cluster summary, the summary area and the
superblock totals.  Superblock, cylinder group and inode check-hashes are
recomputed for everything written.  The file system is marked clean only
after a second, read-only pass finds nothing.

Not handled:
  - A cylinder group whose header is damaged (bad magic, wrong index) is
    reported and makes the repair stop short: no group is rebuilt from
    scratch.
  - A missing or non-directory root inode is not recreated.
  - A missing "." or ".." with no room to put it back is reported, not
    fixed (fsck_ffs cannot fix that case either).
  - Soft updates journals (SU+J), snapshots and extended attribute
    contents are not checked; UFSCore uses none of them.  Snapshots are
    reported, because UFSCore will not write such a disc.
  - Memory: one bit per fragment plus twelve bytes per initialised inode.
    A 1TB file system with 4K fragments wants about 32MB for the map.
  - The independent checkers the tests use (the tree's tests/ufscache
    checker and ufscheck) do not check directories or link counts; those
    rest on DiscMedic's own second pass and UFSCore's reader walking every
    directory.

btrfs (0.10)
------------

The superblock copies are checked (magic, CRC32C checksum, where each says
it is, that they belong to one file system).  Everything else is a scrub by
the BtrFS filing system (BtrFS_Scrub), which needs that module running with
the volume mounted; a scrub rewrites a bad copy from a good one when the
volume is writable.  There is no btrfs check of the trees' structure, and
no repair beyond the scrub's.  Not yet tested on RISC OS: BtrFS is on the
btrfs-port branch and is not in the ROM this was built with.

The configuration memory (0.10)
-------------------------------

-cmos shows, checks and mends the checksum and resets to the RISC OS 64
kernel's defaults (kernel/c/io/cmosdefaults.c), optionally keeping the
network, boot and screen settings (the address ranges are in c/cmos, from
hdr/CMOS).  The mouse type, the station number, the year and everything
from the checksum up are never reset.  The defaults are RISC OS 64's; on
RISC OS 5 a reset is the same table, which is close to but not exactly
that machine's.  Save and restore use *SaveCMOS and *LoadCMOS.


0.11 (22 October 2026): what was changed, and how far it is verified
--------------------------------------------------------------------

An audit of the FileCore and FAT repair paths found ways for a repair to
damage a disc.  Fixed, each with a test that fails without the fix:

  FileCore
  - A map whose first copy had a bad check byte could be "found" by the
    window search at the second copy, and a repair would then write a map
    over the root directory.  A map found by search is no longer written to.
  - The undo file is now flushed (OS_Args 255 on RISC OS, fsync on the
    host) before each sector is written over, each block carries a
    checksum, a torn tail is skipped, and an existing undo file is never
    overwritten.  Old DMUNDO01 files still restore.
  - A repair re-reads the map after mending it and only walks or writes
    directories if the map is then sound.
  - A directory's check byte or sequence number is only rewritten when the
    directory's structure validates; Lost+Found needs a fully valid root and
    is written map first, then directory, then root.
  - The E-format directory check byte now follows FileCore's own
    TestDirCheckByte (the old code summed the wrong bytes) and the boot block
    checksum its CheckSum order.  Both were compared with an independent
    model of FileCore's source on 440 cases.  Defect-list check bytes are
    reported but no longer rewritten: their algorithm is not documented.
  - 4096-byte-sector FileCore discs are checked; share size is applied.
  Verified on: synthetic E, E+ and 4K images (tests/fcore-synth-test.sh) and
  on the real 8GB E+ disc image (no faults, byte-identical output before
  and after, and a repair-and-undo round trip).  NOT verified: any real E
  (Nick) disc - E-format directory check bytes rest on the source and the
  model alone.

  FAT
  - A repair no longer frees "lost" clusters when any directory could not be
    read or reached, and aborts without writing when the table cannot be
    read in full.
  - A root cluster the table shows as free is not repaired into a root.
  - Backup and primary boot sectors that differ in the geometry are left as
    they are rather than one being copied over the other.
  - The clean-shutdown flag is only set when everything found was put right.
  Verified with read-failure injection (tests/fat-fail-test.sh).

Boot partition and CMOS backup (new).  c/bootpart reads and writes the root
directory of the first FAT volume with DiscMedic's own FAT code.  It is
tested on FAT12, FAT16 and FAT32 images, 512 and 4096 byte sectors, a
partitioned device, a full root directory, a directory of the same name and
a looped root, a FileCore disc from sector 0 whose partition table holds the
FAT boot partition (the RISC OS Pi card layout), and every result is checked by fsck.vfat.  Not covered: long
file name collisions with a generated short alias, cross-linked chains
other than into the root, a power cut part way through an overwrite (the
CMOS file's own checksum catches a torn file; the undo file puts it back),
and a power cut mid-write.  On real hardware the boot partition of the Pi4's
SD card is found and listed (a 64MB FAT16 volume, PiBoot, at sector 76, with
its existing CMOS file read and its checksum confirmed); backup and restore
themselves have only been run on images.  The OS-side steps
(*SaveCMOS, *LoadCMOS) are the ones the CMOS menu already used.

Window.  Toolbox32's ProgressBar is used where the Window module has it;
elsewhere a slider.  PartitionBar and Tabs are not used.  A report that the
program's own code at &8000 is sometimes overwritten, which c/gui watches
for, has still not been traced: a read of every front end buffer found no
overflow that explains it.


FileCore: what is reported but not repaired
-------------------------------------------

  - A composite map built from the good zones of two damaged copies is
    detected and reported, but not written.

  - Old map S, M, L and D format discs are recognised as "not something
    DiscMedic handles" and left alone.  They are floppy-era formats.

  - The defect lists are checked for a terminator and a correct check byte.
    The addresses in them are not compared against the defect fragments in
    the map, and DiscMedic will not add a defect to the map.

What it does repair on a FileCore disc: a damaged copy of the map, from the
sound one; the boot block's checksum and defect list terminators; the boot
block's disc record, from the map's; a directory whose check byte is wrong
or whose sequence numbers do not match; and, with "Recover lost data"
selected, objects that no directory names, gathered into a $.Lost+Found
directory built in the root.

Lost+Found recovery takes space off the free chain for the new directory,
gives it an unused object id, writes it, adds an entry for it to the root
in the right place in the directory's order, and writes both copies of the
map back - all through the undo file, so the whole thing can be taken back.
Each recovered object is named File or Dir followed by its object id, and
DiscMedic looks inside it to decide which: a recovered directory is given
the directory attribute so that it can be opened.  Their real names are not
recorded anywhere on the disc once the entry that held them has gone, and a
recovered file has no filetype, so it is typed as Data.

Limits of the recovery as it stands:

  - It only builds a Lost+Found on an E+ or F+ disc, the kind with big
    directories.  On an older E or F disc the lost objects are listed but
    not gathered, because that code cannot be tested here.
  - The root directory must have room for one more entry.  If it is full
    DiscMedic says so and leaves the disc alone rather than growing it.
  - A Lost+Found from an earlier run is left alone and a new one made
    beside it, Lost+Found1 and so on.
  - Where the new entry goes in the root is decided by upper-casing both
    names and comparing bytes, which is what FileCore does - but only the
    ASCII range is handled, and a machine whose territory upper-cases the
    top-bit-set range differently could sort it next to an accented name
    slightly differently from the way FileCore would.


FAT: lost data is freed, not recovered
--------------------------------------

Lost cluster chains are found and reported, and a repair frees them by
default.  Selecting "Keep lost data rather than freeing it" makes DiscMedic
report each chain and leave it allocated, but it does not yet write the
directory entries that would turn those chains into FOUND.000 style files.


FAT: cross-linked files
-----------------------

When two files share a cluster, at least one of them has already lost data.
DiscMedic truncates the second chain it meets, which leaves both files
readable and the volume consistent, and says which files were affected.  It
does not try to work out which file the shared clusters really belonged to,
because nothing on the disc records that.


Reaching the drive
------------------

DiscMedic talks to a drive through its filing system's DiscOp64 SWI, which
every FileCore module for RISC OS 3.50 or later provides, and falls back to
the older DiscOp on drives below 512MB.  It does not supply an alternative
disc record, so a drive whose format FileCore cannot make any sense of at
all may not answer.  If that happens, take an image of the drive with
another machine and check the image.

GUID partition tables are read.  DiscMedic checks the header's own CRC and
the CRC over the entry array before believing either, and says so when they
do not match rather than acting on a damaged table.  Two things it does not
do: it reads only the primary table at LBA 1, never falling back to the
backup copy at the end of the drive, and it cannot describe a partition that
starts or ends beyond the 32-bit sector range, which it reports and skips.
Neither has been exercised on a real GPT drive: the parsing was tested on a
parted-made image on the host, and the same image checked on RISC OS through
-image on the Titanium emulator, which gave identical results.

A drive RISC OS has formatted is a different matter, and the Pi's NVMe drive
settled it.  Such a drive is a FileCore disc from its first sector: it keeps
a boot sector that looks like a FAT one so the firmware can read it, and it
may still carry the partition table of whatever prepared it first - on that
drive a whole-disc GPT written by Gparted, whose entry array FileCore's own
boot block at &C00 partly overwrites, which is why the array's checksum does
not match.  FileCore looks only at its boot block, and so does DiscMedic
now: a drive whose boot block passes its own check byte is read as a
FileCore disc and the table is reported but left alone.  Following the table
instead checked 223GB of nothing.  The same is true of a Pi's SD card, whose
RISC OS volume was being reported as unrecognised for the same reason.

The disc name comes from the map's copy of the disc record, not the boot
block's: SDFS and NVMeFS leave the boot block's copy blank, and both write
the name ended with a carriage return rather than padded with spaces.


Checking a disc that is in use
------------------------------

Nothing stops a check of the disc the machine is running from, and on the
Pi's 223GB NVMe drive a full check of 21412 files in 5030 directories took
about a minute and found one fault: a directory whose check byte does not
match its contents.  Two things to keep in mind about a result like that.
The disc is live, so anything written while the check is reading it may be
seen half done; a fault that appears in one run and not the next is that,
not damage.  And RISC OS holds directories in memory, so what FileCore shows
you need not be what is on the disc - the directory above still catalogues
without complaint.  A fault that reappears run after run, as this one does,
is on the disc.  Check again after a reset before repairing anything.


Where the FileCore map is, and how an object id becomes a place on the disc
--------------------------------------------------------------------------

This was the part that held up the directory scan, and it is now settled
from FileCore's own documentation in the RISC OS sources
(FileSys/FileCore/Doc/EMaps and Doc/EAddrs) rather than from any
second-hand description:

  - The map is nzones sectors long, starting at the beginning of zone
    nzones/2 rounded down.

  - Zone 0's header is 64 bytes; every other zone's is 4.  zone_spare is
    the gap between the last allocation bit of one map block and the first
    of the next - which is the next block's header - so a zone's allocation
    bits run to the END of its sector, and there are
    sector_size*8 - zone_spare of them.  Reading that as "they stop at
    sector_size*8 - zone_spare" makes every zone look as though its last
    fragment has no terminator, which is exactly what happened here first.

  - A bit's offset into the allocation bits, with every non-allocation part
    of the map removed, times the bytes per map bit, is the byte address on
    the disc.

  - A fragment is idlen bits of id, then zeros, then a single 1 bit.  A
    free fragment's "id" is the bit offset to the next free fragment in the
    zone, and that link is only min(idlen, 15) bits wide.

  - A disc address is ddd00000 0ooooooo oooooooo ssssssss: three bits of
    drive, 21 bits of object id, and a sector offset which counts from 1,
    or is 0 when the object is not shared.  An empty file has nothing
    allocated to it at all and is recorded as &00000001.

  - An object's fragments are joined starting from zone
    (id / ids_per_zone), going up and wrapping round the disc.

Checked against a RISC OS formatted 8GB disc: 2065 zones, 32 spare bits, a
1024 byte allocation unit, 19 bit ids, map at sector 8387136, root at
&03325801 which resolves to sector 8391266.

Note that the images made by mkfilecore.py in the overnight-disc project do
NOT follow this: they put the map at sector 0 and use the disc address as a
plain byte address.  They are useful for testing the boot block and map
code and nothing else.


What has been tested, and what has not
--------------------------------------

Tested, with the engine built for RISC OS by the Norcroft compiler:

  - A RISC OS formatted 8GB FileCore E+/F+ hard disc, checked end to end:
    52394 files in 13344 directories nested 19 deep, every object reachable
    from the root, no faults.  That is 13344 directory check bytes computed
    and matched, and 52394 disc addresses resolved through the map, with no
    false positives.
  - The same disc with its root directory's check byte and sequence number
    damaged: both faults found, the directory mended and written back, a
    second check clean, and the undo file put the damage back exactly.
  - The same disc with a 204KB file's entry taken out of the root, leaving
    its object with nowhere pointing at it: the orphan found, gathered into
    a new $.Lost+Found, and the disc then checking clean.  RISC OS was then
    booted from that disc and FileCore itself listed
    ADFS::4.$.Lost+Found.File0002C8E9 as a 204 kbyte Data file - which is
    the only proof that matters that the map allocation, the new directory
    and the root insertion follow FileCore's own rules.  The undo file put
    the disc back to having the orphan again.
  - FAT16 and FAT32 volumes made by mkfs.vfat inside an MBR partition
    table, checked clean; then with five faults injected - a wrong FSInfo
    free count, a divergent second FAT, two lost cluster chains, an
    out-of-range table entry and a cleared clean-shutdown flag - all five
    found, all five repaired, fsck.fat then reported the volume sound, and
    the undo file restored the image byte for byte.
  - The desktop front end on RISC OS 5.31, driving that same full check of
    the 8GB disc end to end: the icon bar icon and menu, the disc list, the
    three scan modes, the repair and undo options, the progress bar, and the
    report, which scrolled itself to the end and wrapped its long lines.

One thing to be careful of, found by running the recovery on the emulator:
the undo file must not be on the disc being repaired, and the check for
that has to canonicalise the path first.  A path like
"<Wimp$ScrapDir>.DiscUndo" - which is what the window offers by default -
says nothing about which disc it names until the filing system has expanded
it, and the original check compared the text, so it let that straight
through.  Both the engine and the front end now canonicalise both sides
with OS_FSControl 37 and compare the filing system and disc.

Verified on RISC OS 5.31, both ways round:

    *DMEngine -f -u <Wimp$ScrapDir>.DiscUndo ADFS 4
    * The undo file <Wimp$ScrapDir>.DiscUndo is on the disc being repaired.
      Put it on a different disc, or in a RAM disc.

    *DMEngine -f -u TiBoxFS::TiBoxFS.$.DiscMedic.undo ADFS 4
      ADFS drive 4, 512 byte sectors
    * Cannot create the undo file TiBoxFS::TiBoxFS.$.DiscMedic.undo.

The first refused before touching the disc; the second was let through and
then failed for the honest reason that the host filing system is read only,
which is what proves the check did not stop it.  The engine is where this
has to hold, because the window runs the engine.  The same check in the
front end, which only exists to say so a moment earlier, could not be
exercised: that emulator instance stopped delivering mouse button events.

Two things worth knowing about the front end, both found by running it:

  - A TextArea must never be given a caret position past the end of what it
    is holding; it does not survive it.  Ask it how much text it has with
    textarea_get_text rather than assuming it matches your own copy.
  - Looking an object up in the map by walking every fragment is O(n) per
    file, which on a disc with fifty thousand files and twenty-six thousand
    fragments turns a one-second check into an hour.  The fragments are
    sorted by object id once, into the order FileCore joins them, and looked
    up by binary search.

Not tested:

  - A repair of a real drive, as opposed to an image file or the
    emulator's disc.
  - An E/F disc with New (2048 byte, 'Nick'/'Hugo') directories.  That code
    is written from the same documentation as the big directory code and
    shares the walk, but no such disc has been put through it.
  - A disc with more than one FileCore partition.

Safety audit, 2026-09-18
------------------------

Prompted by "this app needs double checking to make sure it doesn't destroy
data".  Four things were found and fixed.

1.  A device was writable by default.  disc_write() has always refused to
    write to a device opened read only, and main.c says in a comment that a
    check opens the disc read only - but new_device() set writable = 1 and
    disc_open_drive() never cleared it, so for a real drive the guard could
    not fire.  Several repair helpers (fcmap_flush, fcmap_object_io,
    save_directory, the boot-record rewrite) do not re-test the repair flag
    themselves; they trust their callers.  A device now starts read only and
    exactly one line, in main, opens it up, and only for -f or -r.

2.  The undo file's "not on the disc being repaired" guard could fail open.
    It built a path from the filing system's MODULE name, but RISC OS paths
    use the name the filing system registered: SCSIFS registers with
    FileCore as "SCSI" (FileSys/SCSIFS/SCSIFS/s/ScsiFs00) and RamFS's disc
    is RAM::RamDisc0, so "SCSIFS::4.$" and "RamFS::0.$" expand for neither
    and the guard concluded "different disc".  It now identifies discs by
    NAME - the drive's own, against the disc name in the canonical form of
    the undo path - and, when it cannot establish either, refuses.

3.  A stack buffer could be overrun from the command line: drive_name() and
    same_disc() formatted "%s::%d.$" from an unbounded argv string into a
    32- and 64-byte buffer.  The filing system name is now length checked
    where it is accepted, and the formatting is bounded.

4.  The same-disc guard is not applied to a disc image against itself - for
    an image the question is whether the undo file is on the same disc as
    the image file, and that is what is now compared.

Regression tests live in tests/ and run on the host in seconds:
tests/hostbuild.sh builds the engine with gcc from the shipped sources (no
copies, so the tests cannot drift), and tests/run-tests.sh exercises check,
repair, undo and the refusals against FAT32 images.  The first two tests are
the important ones: a check of a healthy image and a check of a damaged one
both leave the image byte for byte identical.  fsck.fat is used as an
independent second opinion on the repair.

FileCore is covered too, by tests/fcore-test.sh.  Nothing on the host can
produce a canonical FileCore disc - mkfilecore.py does not produce what RISC
OS produces - so that test runs against a real disc image you supply, named
by DM_FCORE_IMAGE or left at tests/images/fcore.img.  A copy of a RISC OS
hard disc is what it is meant for.  It reads the map's position out of the
engine's own report rather than assuming one disc's geometry, damages the
boot block's checksum and one byte of the second map copy, and puts every
byte it touched back at the end, so the image is reusable and is left
exactly as it was found.

Run against a copy of the 8GB Titanium disc (52394 files in 13344
directories, nested 19 deep) it checks the whole disc in 0.13 seconds, finds
both injected faults precisely - "The boot block checksum is &AD and should
be &F7", "Map copy 2, zone 0 is bad (check byte &1B, should be &BE)" -
repairs them, re-checks clean, and the undo file restores the disc byte for
byte.  The check itself was verified to write nothing by comparing the whole
8GB image with the original: identical.

What is still not covered anywhere: repair on a real physical drive rather
than an image, old-map S/M/L/D discs, E/F New-directory discs, composite map
rebuild from two damaged copies, and FAT lost-chain recovery as FOUND.000
files.

The front end's intermittent crash: the emulator, not the program
-----------------------------------------------------------------

The desktop front end occasionally died with an undefined instruction at an
address inside its own code - &0000AFF0 in one capture, where the shipped
binary holds valid ARM (MOV R0,#0 / MOV PC,LR).  It was chased a long way as
a memory-corruption bug in DiscMedic.  It was not one.

The Titanium emulator needs QEMU_RISCOS_PTFIX=1 in its environment.  From
accel/tcg/cputlb.c: applications run in the application slot at &8000+, and
"when AMBControl remaps a slot page to a different physical frame, QEMU must
drop the stale softmmu TLB entry and any translation blocks compiled from
the old/new frames, or it goes on executing stale code".  run-titanium.sh
sets it; the hand-written command line used for this testing did not - the
same omission as TITANIUM_CMOS and TITANIUM_HOSTFS, which had already been
found and put right.  Executing stale code in the application slot is
exactly an undefined instruction at a valid address.

With the flag restored, sixteen mouse-free starts produced no crash and no
crash record; on an iMX6 the program came up, listed two discs, ran a check
of a 111GB disc, was stopped and restarted, and survived thirteen rescans,
with no crash.  That is not proof - the crash was only a few per cent per
run - but the mechanism, the emulator's own documentation of it and the
behaviour on hardware all agree.

What was ruled out along the way, and is worth keeping: gcc at -Wall
-Wextra -fanalyzer is clean on all three front-end files
(tests/frontend-lint.sh; Norcroft is built with -Wp and had missed a stack
overrun in the engine that gcc found at once); every gadget template matches
the Toolbox's own structures and the 18 gadgets end exactly at the end of
the body; every library veneer this program passes NULL to tests for it;
TaskWindow addresses its messages to the parent task rather than
broadcasting them; and the front end's own logic is clean under
AddressSanitizer and UBSan against adversarial engine output (tests/fuzz).

Five changes were made while chasing it, each worth keeping on its own
merits: Wimp_StartTask is no longer called from inside a Toolbox event
handler (it polls the Wimp, and re-entering the event library part way
through a dispatch is not safe); Stop gives up on an engine that never
answers; the TaskWindow message handlers act only on this copy's own
engine; the listing run asks for 2048K rather than 8192K (512K was tried
first and is too small for the engine to start in); and the program keeps a
sum of its own code, saying so and stopping if it ever changes rather than
running on into undefined behaviour.

A crash recorder ships with it: on a fatal signal it writes the signal, the
poll and message counts, and a trail of the last twelve places the program
reached, to <DiscMedic$Dir>.DMCrash or <Wimp$ScrapDir>.DMCrash.


A directory that contains itself made the check run away
--------------------------------------------------------

Found while looking into a check of a 111GB disc that did not finish.  The
walk reported an object reached twice as a fault but still walked into it,
so a loop in the directory tree was followed again at every level down to
the depth limit of 40.  That is not an infinite loop but a multiplication:
on the 8GB test disc one loop back to the root turns a 0.13 second check
into 1.9 seconds, fifteen times the work, and loops compound.

The walk now keeps the disc addresses of the directories on the way down
and refuses to enter one that is already on that path, naming it: "$.X.Y is
inside itself: it is the same directory as an earlier part of its own path.
Not following it."  tests/dirloop.py makes such a loop in a real image (and
mends both the entry and the directory's check byte afterwards), and
tests/fcore-test.sh uses it: the looped check must finish and must name the
directory.

Whether this is what stopped the 111GB check finishing is NOT established -
that machine was turned off before it could be retested.  It is a real bug
with the right shape, found on the way.


A long check now says how far it has got
----------------------------------------

Prompted by "we need to see some file count/directory action in the UI so we
know it's doing something, otherwise it looks like a hang" - which is fair,
and is how the 111GB check was first read.

The directory walk set its phase ("Checking files and directories", 70 to 92
per cent) and then said nothing at all until it finished: the bar sat at 70,
the words never changed, and a walk of fifty thousand files is minutes of
silence.  Both walks now report as they go, every 200 objects:

  |P 70 Checking files and directories - 149 files in 51 directories
  |P 71 Checking files and directories - 716 files in 484 directories
  ...
  |P 92 Checking files and directories - 52285 files in 13319 directories

The front end already puts the text of a |P line in the status field, so the
counts appear there as they climb.  rep_note() in c/report repeats the
current percentage with fresh words; fcmap_objects() counts the distinct
objects the map holds, which is what the FileCore bar measures against, so
the bar moves as well as the words.  The FAT walk has no such count to
measure against - the bar stays at the start of its band - but the words
carry the counts just the same.


NVMe and SD cards, 2026-09-19
------------------------------

Reported: "doesn't see or like NVMe or SD card drives".  Three things were
found.  The first is the dangerous one.

1.  A small FAT32 volume was read as FAT16.  The type was decided purely by
    the cluster count, which is what Microsoft's specification says, and the
    code said so in a comment.  But a FAT32 volume keeps its FAT size in the
    32-bit field and leaves the 16-bit one zero, and a volume made that way
    IS FAT32 whatever the cluster count says.  Small FAT32 volumes below the
    65525 cluster line are ordinary: "mkfs.vfat -F 32" makes one, and so do
    cameras and phones on smaller cards - which is to say, SD cards.

    Reading one as FAT16 invented five faults that were not there.  Worse,
    repairing it would have written 16-bit entries over a 32-bit table and
    destroyed the volume.  The type is now taken from the boot sector's FAT
    size fields when they are unambiguous, and from the cluster count
    otherwise, which is how Linux decides it (fs/fat/inode.c).  Tested both
    ways: -F 32 reads as FAT32 and is clean, -F 16 still reads as FAT16.

2.  512 byte sectors were assumed for any drive FileCore could not describe
    - which is every drive holding only a FAT partition.  A drive formatted
    with 4K logical sectors, as NVMe drives often are, refuses a 512 byte
    read, so the probe failed and the drive never appeared in the list at
    all.  Both the probe and the real open now try 512, 4096, 2048 and 1024
    until one answers, quietly, so an empty drive still says nothing four
    times over.  A disc image dumped from such a drive can be checked with
    the new -sector option.

    The engine already refused to check a volume whose boot sector and drive
    disagreed about the sector size, rather than misreading it, and that
    guard is still there and tested.

3.  Discovery said nothing about what it passed over.  It walks the module
    chain looking for a <name>_DiscOp SWI, and a filing system whose SWIs
    are not named after its module was simply skipped in silence - so "my
    drive is not listed" could not be told from "my drive is empty".  It now
    says, verbosely, every module it looked at, why each was passed over,
    how many were disc filing systems, and what each drive answered.  The
    SWI name search also tries the module name with "FS" added and with a
    trailing "FS" taken off, since SDFS has SDFS_DiscOp while SCSIFS
    registers with FileCore as "SCSI".

What is NOT established: whether the NVMe filing system offers a DiscOp SWI
at all.  There is no NVMe module in the RISC OS sources here or in the
Titanium ROM, only the ADFSFiler build switch that makes an NVMeFiler from
the same sources as ADFSFiler and SDFSFiler, which suggests it is a
FileCore filing system of the usual shape.  If it is, it will be found.  If
its drives still do not appear, this is the command that will say why:

    *DiscMedic -list -v

run on the machine that has the drive.  SD cards were checked as far as they
can be here: SDFS is found correctly on the emulator (SDFS_DiscOp &59040,
SDFS_DiscOp64 &5904C, SDFS_DescribeDisc &59045) and each of its drives
probed; there was simply no card in them.


An edited resource file, 2026-09-19
------------------------------------

The window was redesigned in ResEd and two controls stopped working: the
scan-type radios did nothing, and a quick check walked the directories
anyway.  Two separate causes.

A labelled box draws an icon over its whole rectangle, and the Wimp gives a
click to the icon made last.  In the edited file the box round the scan
options came AFTER the radios and option buttons in the gadget list, and
encloses them, so it sat on top of them and took their clicks: the controls
were drawn, looked right, and did nothing.  Nothing about the radios
themselves was wrong - same component ids, same event, same group, same
flags as the generated file.

tools/orderres.py moves each labelled box to just before the first gadget it
encloses, changing no position, size or property, and remaps the relocation
table for the bytes that moved.  tools/checkres.py now refuses a file where
a box encloses a gadget declared before it, and also checks that every
component the program drives exists and is the kind of gadget the program
thinks it is.  tools/dumpres.py prints a window's gadgets - type, component,
event, group, flags, box - for comparing two files.

The quick check was a plain bug and had nothing to do with the resources:
fcore_check() was never given the quick flag, so "quick" was quick on FAT
and a full walk on FileCore, which is every RISC OS hard disc.  It now stops
after the boot block, the disc record and the map - 0.012 seconds against
0.129 on the 8GB disc - and says in the report what it did not look at,
rather than leaving "every directory has been checked" standing.

An edited resource file now survives the build.  build.sh still runs
mkres.py, because that is what writes h/DMRes - the component ids and event
numbers the program uses - but if Resources/Res,fae exists that is what
ships, and checkres.py is what keeps the two in step.


NVMe: what is and is not established (2026-09-19)
--------------------------------------------------

A -list -v log was taken, but from a machine that does not support NVMe, so
the NVMe stack was not loaded on it and the log says nothing about NVMe
either way.  What it does show is 177 modules, two of them disc filing
systems, and - worth recording - that the SD card WAS found on that machine:
"SDFS 1 FileCore 14GB HardDisk0".  So SD discovery is not the problem; the
FAT32-read-as-FAT16 fault, fixed earlier, is what made SD cards look wrong.

What is established about NVMe, independently of that log:

- NVMeFS presents itself exactly as ADFS, SCSIFS and SDFS do.  From its own
  sources (Sources/FileSys/NVMeFS/NVMeFS/cmhg/NVMeFSHdr) the SWI chunk base
  is &5A600 and the decoding table gives DiscOp, Drives, DescribeDisc,
  MiscOp, SectorDiscOp and DiscOp64 in the usual places; c/nvmefs calls
  FileCore_Create and passes the reference back in R8, so an NVMe drive is a
  FileCore drive like any other.
- With the four modules loaded into the emulator - BlockDeviceNVMe refusing
  with "Unsupported hardware configuration", which is right with no NVMe
  hardware, but NVMeFS loading - DiscMedic found NVMeFS at once, reporting
  NVMeFS_DiscOp64 &5A60C, NVMeFS_DiscOp &5A600 and NVMeFS_DescribeDisc
  &5A605, and probed all eight of its drives.

So the discovery side is proven as far as it can be without the hardware:
the filing system is found and its drives are probed.  What is NOT
established is anything that needs a real drive to answer - whether
DescribeDisc returns a sensible disc record, what sector size comes back,
and whether a partition reads.  Only a -list -v from the machine with the
SSD in it will show that, and until there is one, NVMe should be treated as
unverified.

The module files on the share are compressed, which is why reading their
headers directly shows no SWI chunk: that is the decompression stub's
header, not the module's.
