Input and serial fixes.
This commit is contained in:
parent
89631006f2
commit
91629dc5eb
12 changed files with 408 additions and 49 deletions
232
LLVM816-ASKS.md
232
LLVM816-ASKS.md
|
|
@ -213,3 +213,235 @@ and #$ff`. ABI verified from a compiled probe: pointer lo16 in A, bank in X,
|
||||||
return in A, $e0-$e3 imaginary-register scratch. Any OTHER volatile I/O read
|
return in A, $e0-$e3 imaginary-register scratch. Any OTHER volatile I/O read
|
||||||
through a variable pointer (input/video HALs, future code) is still exposed
|
through a variable pointer (input/video HALs, future code) is still exposed
|
||||||
until the codegen fix lands.
|
until the codegen fix lands.
|
||||||
|
|
||||||
|
## 8. Peeled-first-iteration address negation is one's complement, not two's (2026-08-20) -- FIXED IN TOOLCHAIN same day (see end of item)
|
||||||
|
|
||||||
|
A loop that indexes a local array at a base computed by SUBTRACTING a
|
||||||
|
uint8_t parameter (`k = 8u - nb;` then `t[(uint8_t)(k + i)]` inside a
|
||||||
|
counted loop) is compiled with the FIRST iteration peeled, and that
|
||||||
|
peeled iteration's address is computed as
|
||||||
|
|
||||||
|
base + sign_extend(~nb) ; one byte LOW: ~nb = -nb - 1
|
||||||
|
|
||||||
|
where two's complement (`-nb`) was needed. Every later iteration
|
||||||
|
increments correctly, so exactly the first element is read one byte
|
||||||
|
low and the remaining seven are right -- a silent wrong-VALUES class
|
||||||
|
with no crash, in the same derived-induction-variable family as the
|
||||||
|
modulo-advance item.
|
||||||
|
|
||||||
|
The emitted idiom (RetroNet src/crypto/rnSipHash.c sipRot, w65816
|
||||||
|
-O2 -ffreestanding -ffunction-sections, disassembly of the peeled
|
||||||
|
first read's address):
|
||||||
|
|
||||||
|
lda 0xb6,s ; nb
|
||||||
|
eor #0x1aff ; low byte: nb ^ 0xff = ~nb (high byte junk)
|
||||||
|
and #0xff
|
||||||
|
eor #0x80 ; sign-extend byte
|
||||||
|
clc
|
||||||
|
adc #0xff80 ; (x ^ 0x80) - 0x80
|
||||||
|
...
|
||||||
|
tsc
|
||||||
|
clc
|
||||||
|
adc #0xa3 ; t[] frame base (t + 8 region)
|
||||||
|
clc
|
||||||
|
adc 0x1f,s ; += sext(~nb) --> t + 8 - nb - 1 = t[k-1]
|
||||||
|
|
||||||
|
Blast radius measured before the workaround: EVERY SipHash-2-4 tag the
|
||||||
|
IIgs client computed was wrong (the rotate primitive feeds every round),
|
||||||
|
so every sealed uplink failed server-side authentication. Nothing
|
||||||
|
client-side ever caught it because the protocol's selective encryption
|
||||||
|
leaves downlink frames plaintext -- silent until a peer verifies.
|
||||||
|
Root-caused via on-target known-answer tests + a per-primitive state
|
||||||
|
probe (SIPBLOCK) diffed against the host build of the same source; the
|
||||||
|
"first iteration reads index k-1" model reproduces both on-target wrong
|
||||||
|
tags bit-for-bit end to end.
|
||||||
|
|
||||||
|
WORKAROUND LANDED IN RETRONET: sipRot now takes k (the precomputed read
|
||||||
|
base) instead of nb, removing the 8-nb negation from the function; the
|
||||||
|
emitted first-iteration address becomes a plain base + k and all KATs
|
||||||
|
pass on target. Any other loop whose peeled first iteration derives
|
||||||
|
its address from a subtracted narrow parameter is still exposed.
|
||||||
|
|
||||||
|
RESOLUTION (2026-08-20): root cause was NOT the address lowering -- the
|
||||||
|
IR and ISel were correct (a true NEGA8 pseudo). The AsmPrinter expanded
|
||||||
|
NEGA8 as an UNWRAPPED `EOR #$FF ; INA` on a false "already 8-bit M"
|
||||||
|
assumption: in the real 16-bit-M context the CPU decoded the 2-byte
|
||||||
|
EOR #imm8 as 3-byte EOR #imm16 and swallowed the INA opcode ($1A) as the
|
||||||
|
immediate's high byte -- the disassembly tell is `eor #$1AFF`. FIXED in
|
||||||
|
W65816AsmPrinter.cpp: NEGA8 now emits emitSepM/emitRepM around the pair,
|
||||||
|
exactly like LDA8absX/STA8absX (safe: SepRepCleanup's isMNeutral never
|
||||||
|
extends an M=8 window across the pseudo). Audited: NEGA8 was the only
|
||||||
|
unwrapped Imm8 expansion. Blast radius before the fix: 6 sites in the
|
||||||
|
RetroNet IIgs client -- the SipHash rotate (every sealed uplink failed to
|
||||||
|
authenticate) and 5 rnWidget functions (index corruption; wild-jump BRK
|
||||||
|
during the download offer frame; likely the phantom-FORM garbage too).
|
||||||
|
|
||||||
|
Original ask (kept for the repro): compile the pre-workaround sipRot
|
||||||
|
shape below at -O2 for w65816 and inspect the first read's address chain.
|
||||||
|
(Verbatim pre-workaround function; gSipV is a 40-byte global.)
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void sipRot(uint8_t a, uint8_t nb, uint8_t mode) {
|
||||||
|
uint8_t t[16];
|
||||||
|
uint8_t i;
|
||||||
|
uint8_t k;
|
||||||
|
uint8_t x;
|
||||||
|
|
||||||
|
for (i = 0u; i < 8u; i++) {
|
||||||
|
x = gSipV[(uint8_t)(a + i)];
|
||||||
|
t[i] = x;
|
||||||
|
t[(uint8_t)(i + 8u)] = x;
|
||||||
|
}
|
||||||
|
k = (uint8_t)(8u - nb);
|
||||||
|
for (i = 0u; i < 8u; i++) {
|
||||||
|
x = t[(uint8_t)(k + i)];
|
||||||
|
if (mode == 3u) {
|
||||||
|
x = (uint8_t)((x >> 3) | (uint8_t)(t[(uint8_t)(k + i + 1u)] << 5));
|
||||||
|
} else if (mode == 1u) {
|
||||||
|
x = (uint8_t)((x << 1) | (uint8_t)(t[(uint8_t)(k + i - 1u)] >> 7));
|
||||||
|
}
|
||||||
|
gSipV[(uint8_t)(a + i)] = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. i8 store to a 1-byte stack slot is a 16-bit STA d,s that overflows the return address (2026-08-20) -- FIXED IN TOOLCHAIN same day
|
||||||
|
|
||||||
|
M=16 is the default mode, so `sta d,s` writes TWO bytes. When the target
|
||||||
|
frame object is only 1 byte (an i8 spill / i8 arg-save) at the TOP slot of a
|
||||||
|
frame, the second byte lands on the caller's saved RETURN ADDRESS low byte and
|
||||||
|
the callee's RTL jumps mid-instruction into a different function. Live proof:
|
||||||
|
RetroNet's rnClientOnFileRecord saving its uint8_t `op` clobbered its own
|
||||||
|
return 0x80->0x01, so its RTL landed inside a different rasterizer switch case,
|
||||||
|
skipped the caller's arg cleanup, and left the DISPLAY record-walk's stack
|
||||||
|
pointer 4 low -> $F6 (big-frame pointer) wrong -> the walk byte-crawled forever
|
||||||
|
on the offer frame (the IIgs download "offer-silence": FILE_OFFER never
|
||||||
|
processed, client never drained the serial).
|
||||||
|
|
||||||
|
Minimal repro: `void fwd(uint8_t op, const uint8_t *r){ sink(op, r); }` at -O2
|
||||||
|
for w65816 emits `sta 0x2, s` (16-bit) to save op adjacent to the return.
|
||||||
|
|
||||||
|
FIX (W65816RegisterInfo.cpp eliminateFrameIndex, STAfi case): when
|
||||||
|
`MFI.getObjectSize(FI) == 1`, wrap the STA_StackRel in SEP #$20 / REP #$20 so a
|
||||||
|
1-byte slot gets a genuine 8-bit store. Only the STORE is a hazard; a 16-bit
|
||||||
|
LOAD of the extra byte is a harmless read (the high byte of an i8 value is
|
||||||
|
ignored), so LDAfi is left 16-bit. Verified: fwd now emits
|
||||||
|
`sep #$20 ; sta 0x1,s ; rep #$20`. Same defect family as ASKS #8 (NEGA8) and
|
||||||
|
the $F8 far-frame bank byte -- "an 8-bit operation emitted without the mode
|
||||||
|
wrap in default-16-bit-M code."
|
||||||
|
|
||||||
|
## 10. Pointer-deref to a GLOBAL hardcodes bank 0 (STZ $E2) under the GS/OS Loader (2026-08-20) -- FIXED IN TOOLCHAIN 2026-08-21 (provenance-based; see end)
|
||||||
|
|
||||||
|
The LDAptr / STAptr / STBptr custom inserter (W65816ISelLowering.cpp) lowers a
|
||||||
|
[dp],Y indirect-long deref with the BANK byte forced to 0 via `STZ $E2` (unless
|
||||||
|
the LoaderBankDeref path, which uses $BE, is taken). Bank-0 is correct for a
|
||||||
|
pointer to a STACK local (the 65816 stack is always bank 0) but WRONG for a
|
||||||
|
pointer to a GLOBAL: under the GS/OS Loader the program's BSS/globals are placed
|
||||||
|
in the program bank ($BE, e.g. $0A), not bank 0. So `&someGlobal` dereferenced
|
||||||
|
through this path reads bank-0 garbage, while the SAME global accessed directly
|
||||||
|
via `sta/lda abs` (DBR-relative, DBR=$BE) is correct -- a silent, layout-
|
||||||
|
dependent miscompile. The inserter's own comment already flags this tension
|
||||||
|
(gmtime's __gmtimeBuf hit it).
|
||||||
|
|
||||||
|
Live proof: RetroNet's inlined rnXferRequest materialised `&gX.id` (a static
|
||||||
|
global) and read it through LDAptr; the FILE_REQUEST then carried xferId=0xAD
|
||||||
|
(bank-0 garbage) instead of 1, and the server rejected the download. 0xAD is
|
||||||
|
not any valid gX field -- the tell of a wrong-bank read. Disasm:
|
||||||
|
`lda #<gX.id off> ; sta $e0 ; stz $e2 ; lda [$e0],y`.
|
||||||
|
|
||||||
|
WORKAROUND in app: `noinline` on the function so the global is read in the
|
||||||
|
caller through DBR-relative abs16 and passed by the ABI (same shape as the
|
||||||
|
existing rnXferState workaround).
|
||||||
|
|
||||||
|
Ask: the deref bank cannot be a blanket 0 or blanket $BE when stack pointers
|
||||||
|
(bank 0) and global pointers ($BE) are both derefed through one pseudo. The
|
||||||
|
principled fix is a 24-bit pointer representation (carry the bank in the pointer
|
||||||
|
value) or provenance tracking so a global-derived pointer derefs with $BE and a
|
||||||
|
stack-derived pointer with 0.
|
||||||
|
|
||||||
|
Fixes EVALUATED and rejected from the app side (2026-08-20):
|
||||||
|
1. -w65816-loader-bank-deref (existing flag): whole-program, flips EVERY
|
||||||
|
LDAptr to $BE. Fixes global derefs but breaks stack-pointer derefs (the
|
||||||
|
reason LDAptrBank0 exists for va_arg). Also the runtime *.o are shared
|
||||||
|
across Loader AND non-Loader (GNO/smoke) builds, so it can't be enabled
|
||||||
|
for just the S16/Loader target without a per-mode runtime split.
|
||||||
|
2. Fold (load <materialized global addr>) -> abs16+DBR at selection -- the
|
||||||
|
clean targeted fix, but NOT minimally reproducible: every small repro
|
||||||
|
(inlined forwarder, barrier-forced re-read, struct field) correctly
|
||||||
|
selects abs16. The mis-selection only appears inside a very large inlined
|
||||||
|
function (RetroNet rnFileRecord, ~0x916 bytes, three transfer routines
|
||||||
|
inlined), so it is a scheduling/CSE artifact of address materialization
|
||||||
|
under register pressure, not a stable pattern -- hard to fix safely
|
||||||
|
without provenance.
|
||||||
|
RESOLUTION (2026-08-21, W65816ISelLowering.cpp LDAptr/STAptr/STBptr custom
|
||||||
|
inserter): instead of a blanket bank policy, use PROVENANCE. When the pointer
|
||||||
|
being derefed traces (through COPYs) to an LDAi16imm of a global / external
|
||||||
|
symbol, the deref is exactly a DBR-relative absolute access to that global --
|
||||||
|
the same thing a direct `global` reference compiles to -- so emit LDAabs /
|
||||||
|
STAabs / STA8abs with the global operand and skip the [dp],Y deref entirely.
|
||||||
|
That lands in DBR's bank (correct for a global) with no bank guess, and only
|
||||||
|
GLOBAL-address derefs are touched -- stack pointers (bank 0) and heap/other
|
||||||
|
pointers keep the existing deref path, so no stack-out-param regression. NB the
|
||||||
|
bank canNOT be sourced from $BE: $BE is the code segment's PBR and in a
|
||||||
|
multi-segment program the data (BSS) segment is a different bank; only DBR (via
|
||||||
|
abs) is right. Single-segment builds are unaffected (DBR==0==old STZ bank).
|
||||||
|
Verified: RetroNet's rnXferRequest/rnXferState no longer need `noinline` (both
|
||||||
|
removed) and the inlined rnFileRecord emits zero bank-0 pointer-derefs; the IIgs
|
||||||
|
download DISK-VERIFIES from the pure-clean build. Also fixes the runtime gmtime
|
||||||
|
__gmtimeBuf case this item's parent note describes. What is still NOT covered:
|
||||||
|
a global address that flows through pointer ARITHMETIC the trace can't follow
|
||||||
|
(e.g. `p = &g; p += i; *p`) -- there the provenance is lost and the deref falls
|
||||||
|
back to bank 0; those remain candidates for the 24-bit-pointer rework.
|
||||||
|
|
||||||
|
## 11. ADDframe (stack-slot LEA) scheduled between a CMP and its branch clobbers the flags (2026-08-24) -- FIXED IN TOOLCHAIN same day
|
||||||
|
|
||||||
|
`ADDframe` - the LEA-equivalent that puts the address of a stack slot in A -
|
||||||
|
expands at PEI (W65816RegisterInfo.cpp eliminateFrameIndex) to `TSC; CLC;
|
||||||
|
ADC #disp`. That sequence rewrites N/Z (TSC, ADC), C (CLC, ADC) and V (ADC).
|
||||||
|
The pseudo deliberately carries no `Defs = [P]` (the same reasoning as the
|
||||||
|
LDA pseudos, see the NOTE above LDAi16imm in W65816InstrInfo.td) and is
|
||||||
|
`isReMaterializable`, so a post-RA pass may legally place it between a CMP
|
||||||
|
and the Bxx that consumes the CMP's flags. Machine Copy Propagation did
|
||||||
|
exactly that in RetroNet's src/platform/rnClient.c (main, the widget action
|
||||||
|
dispatch):
|
||||||
|
|
||||||
|
lda 0xf,s ; act.kind
|
||||||
|
cmp #0x3 ; == RN_WA_SUBMIT ?
|
||||||
|
tsc ; \
|
||||||
|
clc ; > ADDframe: &act, hoisted here for the SUBMIT arm
|
||||||
|
adc #0xc1 ; /
|
||||||
|
bne .LBB3_61 ; tests (SP+0xC1) != 0 - ALWAYS taken
|
||||||
|
|
||||||
|
so `act.kind == RN_WA_SUBMIT` could never be true: the LOGIN button
|
||||||
|
activated, rnWidgetActivate wrote kind=3, and rnClientOnSubmit was never
|
||||||
|
reached. On the IIgs the login form was never submitted; the harness's
|
||||||
|
Tab/Return retry loop then produced exactly nine no-widget Return uplinks,
|
||||||
|
which is what pinned it. A scan of every TU in the IIgs client found this
|
||||||
|
ONE instance (`cmp` immediately followed by `tsc` before the branch), which
|
||||||
|
is why only the submit was affected.
|
||||||
|
|
||||||
|
The existing mitigation - the PHP/PLP wrap in W65816StackSlotCleanup.cpp
|
||||||
|
Pass -2.5 - did not catch it for two reasons: (1) ADDframe was not in the
|
||||||
|
pass's corrupting set (isLdaLike), so the backward walk from the branch hit
|
||||||
|
it, took isFlagDefining's default ("anything else defines flags") and called
|
||||||
|
it the TEST, then declined to wrap; (2) the pass only collects BEQ/BNE/BMI/
|
||||||
|
BPL, on the (true) grounds that LDA-like ops leave C alone - but ADDframe
|
||||||
|
also clobbers C and V, so a `cmp; ADDframe; bcc` would have been miscompiled
|
||||||
|
with no protection at all.
|
||||||
|
|
||||||
|
RESOLUTION (2026-08-24, W65816StackSlotCleanup.cpp Pass -2.5):
|
||||||
|
* ADDframe is now in isLdaLike (N/Z-corrupting), so the walk continues
|
||||||
|
past it to the real CMP and wraps it: `cmp #3; php; tsc; clc; adc #disp+1;
|
||||||
|
plp; bne`. It was already in isStackRel and the bump list, so its
|
||||||
|
ImmOffset gets the +1 that compensates PHP's S decrement; the FP-relative
|
||||||
|
exemption in the bump is skipped for ADDframe because it has no FP-
|
||||||
|
relative expansion (always TSC+ADC, i.e. always SP-relative).
|
||||||
|
* BCC/BCS/BVC/BVS are collected as well, with a separate
|
||||||
|
isCarryCorrupting predicate (ADDframe only): under a C/V branch an
|
||||||
|
N/Z-only corrupter is walked past without a wrap, so the size of every
|
||||||
|
existing carry-branch sequence is unchanged.
|
||||||
|
* Pinned by llvm/test/CodeGen/W65816/addframe-flag-wrap.mir (nz, carry,
|
||||||
|
and the lda-under-carry no-wrap case).
|
||||||
|
Verified: the rnClient.c TU rescans clean (zero cmp/tsc/branch sequences in
|
||||||
|
all 20 IIgs client TUs) and the IIgs login submits live - see the RetroNet
|
||||||
|
e2e matrix (`joey-iigs session`).
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,10 @@ typedef enum {
|
||||||
#define JL_CHAR_TAB 0x09
|
#define JL_CHAR_TAB 0x09
|
||||||
#define JL_CHAR_RETURN 0x0D
|
#define JL_CHAR_RETURN 0x0D
|
||||||
#define JL_CHAR_ESCAPE 0x1B
|
#define JL_CHAR_ESCAPE 0x1B
|
||||||
|
// The FORWARD delete (the key labelled Del/Delete on a PC, ST or Amiga keyboard - not backspace). A text
|
||||||
|
// editor needs both: backspace removes the character BEFORE the caret, this one removes the character
|
||||||
|
// UNDER it. 0x7F is ASCII DEL, which is what every one of these keyboards already reports.
|
||||||
|
#define JL_CHAR_DELETE 0x7F
|
||||||
|
|
||||||
// Typed-character queue capacity (ring buffer; one slot stays empty,
|
// Typed-character queue capacity (ring buffer; one slot stays empty,
|
||||||
// so JL_CHAR_QUEUE_SIZE - 1 characters can be pending). Must be a
|
// so JL_CHAR_QUEUE_SIZE - 1 characters can be pending). Must be a
|
||||||
|
|
@ -114,7 +118,7 @@ void jlInputPoll(void);
|
||||||
|
|
||||||
// Pop the next typed character as a 7-bit code, or -1 if the queue
|
// Pop the next typed character as a 7-bit code, or -1 if the queue
|
||||||
// is empty. Codes: printable ASCII 0x20..0x7E, plus JL_CHAR_BACKSPACE,
|
// is empty. Codes: printable ASCII 0x20..0x7E, plus JL_CHAR_BACKSPACE,
|
||||||
// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE. Shift, caps lock, and
|
// JL_CHAR_TAB, JL_CHAR_RETURN, JL_CHAR_ESCAPE, JL_CHAR_DELETE. Shift, caps lock, and
|
||||||
// the keyboard layout are already applied by the backend, so
|
// the keyboard layout are already applied by the backend, so
|
||||||
// punctuation and shifted symbols arrive correctly on every port.
|
// punctuation and shifted symbols arrive correctly on every port.
|
||||||
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS
|
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,8 @@
|
||||||
#define JL_HAS_SERIAL_WRITE
|
#define JL_HAS_SERIAL_WRITE
|
||||||
#define JL_HAS_SERIAL_AVAILABLE
|
#define JL_HAS_SERIAL_AVAILABLE
|
||||||
#define JL_HAS_SERIAL_FLUSH
|
#define JL_HAS_SERIAL_FLUSH
|
||||||
|
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||||
|
#define JL_HAS_SERIAL_SET_RTS
|
||||||
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create
|
// save files + disk space: delete (GS/OS Destroy), dir-ensure (GS/OS Create
|
||||||
// $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0).
|
// $2001, storageType 13), disk-free (GS/OS Volume $2008 via prefix 0).
|
||||||
#define JL_HAS_SAVE_DIR_ENSURE
|
#define JL_HAS_SAVE_DIR_ENSURE
|
||||||
|
|
@ -353,6 +355,8 @@
|
||||||
#define JL_HAS_SERIAL_WRITE
|
#define JL_HAS_SERIAL_WRITE
|
||||||
#define JL_HAS_SERIAL_AVAILABLE
|
#define JL_HAS_SERIAL_AVAILABLE
|
||||||
#define JL_HAS_SERIAL_FLUSH
|
#define JL_HAS_SERIAL_FLUSH
|
||||||
|
// Application-driven RTS for a caller doing its own receive-side flow control (jlSerialSetRts).
|
||||||
|
#define JL_HAS_SERIAL_SET_RTS
|
||||||
// save files + disk space (POSIX mkdir/remove + int21h/36h disk-free)
|
// save files + disk space (POSIX mkdir/remove + int21h/36h disk-free)
|
||||||
#define JL_HAS_SAVE_DIR_ENSURE
|
#define JL_HAS_SAVE_DIR_ENSURE
|
||||||
#define JL_HAS_SAVE_DELETE
|
#define JL_HAS_SAVE_DELETE
|
||||||
|
|
|
||||||
|
|
@ -101,4 +101,16 @@ int16_t jlSerialReadByte(void);
|
||||||
// Discard any buffered received bytes.
|
// Discard any buffered received bytes.
|
||||||
void jlSerialFlush(void);
|
void jlSerialFlush(void);
|
||||||
|
|
||||||
|
// ASSERT OR DROP RTS, for a caller that does its own receive-side flow control.
|
||||||
|
//
|
||||||
|
// JL_SERIAL_FLOW_RTSCTS asks the PORT to handshake; this is the other half - the application saying
|
||||||
|
// "my buffer is nearly full, stop sending" and later "go ahead", which is what a client with a small
|
||||||
|
// ring and a peer that can be told to pause actually needs. RetroNet's 6551 clients (C64, Apple II)
|
||||||
|
// have done exactly this for a long time and the 16-bit tier could not, so a JoeyLib target's only
|
||||||
|
// defence against an overrun was the packet layer's retransmit.
|
||||||
|
//
|
||||||
|
// `on` = true asserts RTS (ready to receive). A port with no controllable RTS line no-ops, and a
|
||||||
|
// closed port no-ops; neither is an error, so a caller can drive it unconditionally.
|
||||||
|
void jlSerialSetRts(bool on);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,9 @@ void jlInputCharPush(uint8_t ch) {
|
||||||
uint8_t head;
|
uint8_t head;
|
||||||
uint8_t next;
|
uint8_t next;
|
||||||
|
|
||||||
if (ch > 0x7E) {
|
// 0x7F is the FORWARD delete, not junk: it is the one code above the printable range the queue
|
||||||
|
// carries, because a text editor needs a key that removes the character under the caret.
|
||||||
|
if (ch > 0x7E && ch != JL_CHAR_DELETE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ch < 0x20 &&
|
if (ch < 0x20 &&
|
||||||
|
|
|
||||||
|
|
@ -1011,6 +1011,16 @@ void jlpGenericSerialFlush(void);
|
||||||
#define jlpSerialFlush() jlpGenericSerialFlush()
|
#define jlpSerialFlush() jlpGenericSerialFlush()
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
// Application-driven RTS (jlSerialSetRts). The generic stub does nothing, which is the honest answer
|
||||||
|
// for a port whose RTS line is not controllable - a caller drives this unconditionally.
|
||||||
|
void jlpGenericSerialSetRts(bool on);
|
||||||
|
#if !defined(jlpSerialSetRts)
|
||||||
|
#if defined(JL_HAS_SERIAL_SET_RTS)
|
||||||
|
void jlpSerialSetRts(bool on);
|
||||||
|
#else
|
||||||
|
#define jlpSerialSetRts(_o) jlpGenericSerialSetRts((_o))
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) ---
|
// --- Save files + disk space (platform-only; blank-port stub generic + JL_HAS override) ---
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,14 @@ int16_t jlSerialReadByte(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void jlSerialSetRts(bool on) {
|
||||||
|
if (!gSerialOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jlpSerialSetRts(on);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len) {
|
uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len) {
|
||||||
if (!gSerialOpen || buf == NULL || len == 0u) {
|
if (!gSerialOpen || buf == NULL || len == 0u) {
|
||||||
return 0u;
|
return 0u;
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@
|
||||||
// (0xE0 0x1C) and keypad '/' (0xE0 0x35).
|
// (0xE0 0x1C) and keypad '/' (0xE0 0x35).
|
||||||
#define SCAN_ENTER 0x1C
|
#define SCAN_ENTER 0x1C
|
||||||
#define SCAN_SLASH 0x35
|
#define SCAN_SLASH 0x35
|
||||||
|
#define SCAN_DELETE 0x53 // shared by keypad '.' and the E0-prefixed Delete key
|
||||||
|
|
||||||
// Private ISR->poll ring for translated characters. Power of two so
|
// Private ISR->poll ring for translated characters. Power of two so
|
||||||
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
|
// the wrap is a mask; holds ISR_CHAR_QUEUE_SIZE - 1 pending bytes.
|
||||||
|
|
@ -351,10 +352,14 @@ static void keyboardIsr(void) {
|
||||||
// commands, not text, so they produce no character.
|
// commands, not text, so they produce no character.
|
||||||
ch = 0;
|
ch = 0;
|
||||||
if (extended) {
|
if (extended) {
|
||||||
// Of the 0xE0 pairs only keypad Enter and keypad '/'
|
// Of the 0xE0 pairs only keypad Enter, keypad '/' and
|
||||||
// type; the rest (arrows, Home/End, ...) do not.
|
// DELETE type; the rest (arrows, Home/End, ...) do not.
|
||||||
|
// Delete is the FORWARD delete a text editor needs -
|
||||||
|
// backspace already types 0x08 from the main block.
|
||||||
if (code == SCAN_ENTER || code == SCAN_SLASH) {
|
if (code == SCAN_ENTER || code == SCAN_SLASH) {
|
||||||
ch = gScanAsciiNormal[code];
|
ch = gScanAsciiNormal[code];
|
||||||
|
} else if (code == SCAN_DELETE) {
|
||||||
|
ch = JL_CHAR_DELETE;
|
||||||
}
|
}
|
||||||
} else if (code >= SCAN_KEYPAD_FIRST && code <= SCAN_KEYPAD_LAST &&
|
} else if (code >= SCAN_KEYPAD_FIRST && code <= SCAN_KEYPAD_LAST &&
|
||||||
gIsrNumLock && gKeypadAscii[code - SCAN_KEYPAD_FIRST] != 0) {
|
gIsrNumLock && gKeypadAscii[code - SCAN_KEYPAD_FIRST] != 0) {
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,23 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RTS on demand (jlSerialSetRts): MCR bit 1. DTR and OUT2 are preserved - OUT2 physically gates this
|
||||||
|
// UART's interrupt to the PIC, so clearing it while dropping RTS would silence the receiver entirely,
|
||||||
|
// which is the opposite of what a flow-control pause is for.
|
||||||
|
void jlpSerialSetRts(bool on) {
|
||||||
|
uint8_t mcr;
|
||||||
|
|
||||||
|
if (gComBase == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mcr = (uint8_t)(MCR_DTR | MCR_OUT2);
|
||||||
|
if (on) {
|
||||||
|
mcr = (uint8_t)(mcr | MCR_RTS);
|
||||||
|
}
|
||||||
|
outportb((int)(gComBase + UART_MCR), mcr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void jlpSerialPoll(void) {
|
void jlpSerialPoll(void) {
|
||||||
// The RX IRQ fills the ring in the background; nothing to pump.
|
// The RX IRQ fills the ring in the background; nothing to pump.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ void jlpGenericSerialPoll(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A port with no controllable RTS line: doing nothing is the honest answer, and it lets a caller drive
|
||||||
|
// jlSerialSetRts unconditionally instead of guarding every call site with a platform test.
|
||||||
|
void jlpGenericSerialSetRts(bool on) {
|
||||||
|
(void)on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
uint16_t jlpGenericSerialRead(uint8_t *buf, uint16_t max) {
|
uint16_t jlpGenericSerialRead(uint8_t *buf, uint16_t max) {
|
||||||
(void)buf;
|
(void)buf;
|
||||||
(void)max;
|
(void)max;
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,10 @@ static uint16_t gRxTail;
|
||||||
static bool gOpen;
|
static bool gOpen;
|
||||||
static bool gIsScc; // true: 8530 SCC; false: 6551 ACIA
|
static bool gIsScc; // true: 8530 SCC; false: 6551 ACIA
|
||||||
static volatile uint8_t *gStatus; // SCC command reg (RR0) / 6551 status reg
|
static volatile uint8_t *gStatus; // SCC command reg (RR0) / 6551 status reg
|
||||||
|
static uint8_t gWr5; // the SCC's live WR5 (Tx bits + DTR + RTS), so jlpSerialSetRts
|
||||||
|
// can flip ONE bit without guessing the rest back: WR5 is
|
||||||
|
// write-only on this chip, and rebuilding it from scratch would
|
||||||
|
// silently drop the data-bit and Tx-enable settings
|
||||||
static volatile uint8_t *gData; // data register (both)
|
static volatile uint8_t *gData; // data register (both)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -219,7 +223,8 @@ static void serialInitScc(volatile uint8_t *ctrl, volatile uint8_t *data, uint8_
|
||||||
sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high
|
sccWrite(ctrl, 13u, (uint8_t)((tc >> 8) & 0xFFu)); // time constant high
|
||||||
sccWrite(ctrl, 14u, 0x03u); // BRG enable, source = PCLK
|
sccWrite(ctrl, 14u, 0x03u); // BRG enable, source = PCLK
|
||||||
sccWrite(ctrl, 3u, (uint8_t)(wr3 | 0x01u)); // Rx enable
|
sccWrite(ctrl, 3u, (uint8_t)(wr3 | 0x01u)); // Rx enable
|
||||||
sccWrite(ctrl, 5u, (uint8_t)(wr5 | 0x8Au)); // Tx enable + DTR + RTS
|
gWr5 = (uint8_t)(wr5 | 0x8Au);
|
||||||
|
sccWrite(ctrl, 5u, gWr5); // Tx enable + DTR + RTS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -352,6 +357,23 @@ void iigsSerialPump(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RTS on demand (jlSerialSetRts). SCC: WR5 bit 1, applied to the retained copy so the Tx enable, DTR and
|
||||||
|
// data-bit fields survive. 6551: the command register's bits 3:2 - %10 asserts RTS with the receiver
|
||||||
|
// interrupt disabled (this HAL polls), %00 deasserts it AND gates the transmitter off, which is why the
|
||||||
|
// deassert is only ever a pause.
|
||||||
|
void jlpSerialSetRts(bool on) {
|
||||||
|
if (!gOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gIsScc) {
|
||||||
|
gWr5 = on ? (uint8_t)(gWr5 | 0x02u) : (uint8_t)(gWr5 & (uint8_t)~0x02u);
|
||||||
|
sccWrite(gStatus, 5u, gWr5);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gStatus[1] = on ? 0x09u : 0x01u; // 6551 command: DTR on, RTS asserted / deasserted
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void jlpSerialPoll(void) {
|
void jlpSerialPoll(void) {
|
||||||
uint16_t tail;
|
uint16_t tail;
|
||||||
uint16_t guard;
|
uint16_t guard;
|
||||||
|
|
|
||||||
124
tools/xdftool.py
124
tools/xdftool.py
|
|
@ -101,8 +101,12 @@ class Xdf:
|
||||||
continue
|
continue
|
||||||
name = e[0:8].decode("ascii", "replace").rstrip()
|
name = e[0:8].decode("ascii", "replace").rstrip()
|
||||||
ext = e[8:11].decode("ascii", "replace").rstrip()
|
ext = e[8:11].decode("ascii", "replace").rstrip()
|
||||||
|
# Human68k EXTENDED names: chars 9-18 of the stem live in bytes 12-21 (see encode_name_ext).
|
||||||
|
# Fold them back in so a listing shows the full name a guest wrote (e.g. the 9-char blob keys).
|
||||||
|
more = e[12:22].split(b"\x00", 1)[0].decode("ascii", "replace").rstrip()
|
||||||
|
stem = name + more
|
||||||
out.append({
|
out.append({
|
||||||
"name": f"{name}.{ext}" if ext else name,
|
"name": f"{stem}.{ext}" if ext else stem,
|
||||||
"cluster": struct.unpack("<H", e[26:28])[0],
|
"cluster": struct.unpack("<H", e[26:28])[0],
|
||||||
"size": struct.unpack("<I", e[28:32])[0],
|
"size": struct.unpack("<I", e[28:32])[0],
|
||||||
"attr": e[11],
|
"attr": e[11],
|
||||||
|
|
@ -214,47 +218,81 @@ class Xdf:
|
||||||
self.data[off] = DELETED_MARKER
|
self.data[off] = DELETED_MARKER
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def add_file(self, name, payload):
|
def _alloc_write_payload(self, payload):
|
||||||
self.delete(name) # overwrite semantics
|
# Allocate a FAT chain for `payload`, write it (zero-padding the final cluster so stale bytes never
|
||||||
need = (len(payload) + self.cluster_bytes - 1) // self.cluster_bytes
|
# leak), and return the head cluster (0 for an empty payload). Shared by add_file and
|
||||||
|
# add_file_in_subdir so the cluster-writing logic lives in exactly one place.
|
||||||
|
cb = self.cluster_bytes
|
||||||
|
need = (len(payload) + cb - 1) // cb
|
||||||
|
if need == 0:
|
||||||
|
return 0
|
||||||
free = self.free_clusters()
|
free = self.free_clusters()
|
||||||
if len(free) < need:
|
if len(free) < need:
|
||||||
raise OSError(f"{self.path}: need {need} clusters, {len(free)} free "
|
raise OSError(f"{self.path}: need {need} clusters, {len(free)} free "
|
||||||
f"({len(free) * self.cluster_bytes} bytes)")
|
f"({len(free) * cb} bytes)")
|
||||||
chain = free[:need]
|
chain = free[:need]
|
||||||
for idx, cluster in enumerate(chain):
|
for idx, cluster in enumerate(chain):
|
||||||
off = self.cluster_offset(cluster)
|
off = self.cluster_offset(cluster)
|
||||||
chunk = payload[idx * self.cluster_bytes:(idx + 1) * self.cluster_bytes]
|
chunk = payload[idx * cb:(idx + 1) * cb]
|
||||||
self.data[off:off + len(chunk)] = chunk
|
self.data[off:off + len(chunk)] = chunk
|
||||||
# Zero the tail of the final cluster so stale bytes never leak.
|
if len(chunk) < cb:
|
||||||
if len(chunk) < self.cluster_bytes:
|
self.data[off + len(chunk):off + cb] = bytes(cb - len(chunk))
|
||||||
self.data[off + len(chunk):off + self.cluster_bytes] = \
|
|
||||||
bytes(self.cluster_bytes - len(chunk))
|
|
||||||
self.fat_set(cluster, 0xFFF if idx == need - 1 else chain[idx + 1])
|
self.fat_set(cluster, 0xFFF if idx == need - 1 else chain[idx + 1])
|
||||||
|
return chain[0]
|
||||||
|
|
||||||
|
def _put_entry_in_dir(self, first_cluster, entry):
|
||||||
|
# Write a 32-byte directory `entry` into the first FREE/DELETED slot of the directory whose data
|
||||||
|
# begins at first_cluster, extending the cluster chain by one when every slot is already used.
|
||||||
|
per = self.cluster_bytes // DIR_ENTRY_SIZE
|
||||||
|
cluster = first_cluster
|
||||||
|
last = cluster
|
||||||
|
guard = 0
|
||||||
|
while 2 <= cluster < EOC_MIN:
|
||||||
|
base = self.cluster_offset(cluster)
|
||||||
|
for i in range(per):
|
||||||
|
slot = base + i * DIR_ENTRY_SIZE
|
||||||
|
if self.data[slot] in (FREE_MARKER, DELETED_MARKER):
|
||||||
|
self.data[slot:slot + DIR_ENTRY_SIZE] = entry
|
||||||
|
return
|
||||||
|
last = cluster
|
||||||
|
cluster = self.fat_get(cluster)
|
||||||
|
guard += 1
|
||||||
|
if guard > self.max_cluster:
|
||||||
|
raise OSError(f"{self.path}: directory cluster chain loops")
|
||||||
|
# Every slot is used: append a fresh, all-free cluster and write into its first slot.
|
||||||
|
free = self.free_clusters()
|
||||||
|
if not free:
|
||||||
|
raise OSError(f"{self.path}: no free cluster to extend directory")
|
||||||
|
newc = free[0]
|
||||||
|
self.fat_set(last, newc)
|
||||||
|
self.fat_set(newc, 0xFFF)
|
||||||
|
base = self.cluster_offset(newc)
|
||||||
|
self.data[base:base + self.cluster_bytes] = bytes(self.cluster_bytes)
|
||||||
|
self.data[base:base + DIR_ENTRY_SIZE] = entry
|
||||||
|
|
||||||
|
def add_file(self, name, payload):
|
||||||
|
self.delete(name) # overwrite semantics
|
||||||
|
# encode_name_ext writes the Human68k EXTENDED name (a strict superset of 8.3: ext10 is all-NUL
|
||||||
|
# for a plain 8.3 name), so names up to 18 chars - like RetroNet's 9-char blob keys - land in a
|
||||||
|
# form the guest and xdftool's own find()/read_file() both resolve.
|
||||||
|
main11, ext10 = self.encode_name_ext(name)
|
||||||
|
head = self._alloc_write_payload(payload)
|
||||||
slot = None
|
slot = None
|
||||||
for i, off, e in self._entries():
|
for _, off, e in self._entries():
|
||||||
if e[0] in (FREE_MARKER, DELETED_MARKER):
|
if e[0] in (FREE_MARKER, DELETED_MARKER):
|
||||||
slot = off
|
slot = off
|
||||||
break
|
break
|
||||||
if slot is None:
|
if slot is None:
|
||||||
raise OSError(f"{self.path}: root directory full ({self.root_entries} entries)")
|
raise OSError(f"{self.path}: root directory full ({self.root_entries} entries)")
|
||||||
|
self.data[slot:slot + DIR_ENTRY_SIZE] = self._dir_entry(
|
||||||
|
main11, ATTR_ARCHIVE, head, len(payload), ext10)
|
||||||
|
|
||||||
entry = bytearray(DIR_ENTRY_SIZE)
|
def _dir_entry(self, name11, attr, cluster, size, ext10=None):
|
||||||
entry[0:11] = self.encode_name(name)
|
|
||||||
entry[11] = ATTR_ARCHIVE
|
|
||||||
# Fixed timestamp: reproducible images matter more than real mtimes,
|
|
||||||
# since these get byte-compared across gate runs.
|
|
||||||
struct.pack_into("<H", entry, 22, (12 << 11)) # 12:00:00
|
|
||||||
struct.pack_into("<H", entry, 24, ((2026 - 1980) << 9) | (1 << 5) | 1)
|
|
||||||
struct.pack_into("<H", entry, 26, chain[0] if need else 0)
|
|
||||||
struct.pack_into("<I", entry, 28, len(payload))
|
|
||||||
self.data[slot:slot + DIR_ENTRY_SIZE] = entry
|
|
||||||
|
|
||||||
def _dir_entry(self, name11, attr, cluster, size):
|
|
||||||
entry = bytearray(DIR_ENTRY_SIZE)
|
entry = bytearray(DIR_ENTRY_SIZE)
|
||||||
entry[0:11] = name11
|
entry[0:11] = name11
|
||||||
entry[11] = attr
|
entry[11] = attr
|
||||||
|
if ext10 is not None: # Human68k extended name
|
||||||
|
entry[12:22] = ext10
|
||||||
struct.pack_into("<H", entry, 22, (12 << 11)) # 12:00:00
|
struct.pack_into("<H", entry, 22, (12 << 11)) # 12:00:00
|
||||||
struct.pack_into("<H", entry, 24, ((2026 - 1980) << 9) | (1 << 5) | 1) # 2026-01-01
|
struct.pack_into("<H", entry, 24, ((2026 - 1980) << 9) | (1 << 5) | 1) # 2026-01-01
|
||||||
struct.pack_into("<H", entry, 26, cluster)
|
struct.pack_into("<H", entry, 26, cluster)
|
||||||
|
|
@ -262,36 +300,34 @@ class Xdf:
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
def add_file_in_subdir(self, dirname, name, payload):
|
def add_file_in_subdir(self, dirname, name, payload):
|
||||||
# Create a one-cluster subdirectory in root (with . and .. entries) and write `name` into it, so a
|
# Write `name` into subdirectory `dirname` (creating SUB/. and SUB/.. when the subdir is new), so a
|
||||||
# guest that reads e.g. SAVES/RN.CFG finds it. Fresh-template semantics: the subdir must not exist
|
# guest reading e.g. SAVES/RN.CFG or SAVES/<blob> finds it. If the subdir ALREADY exists (a second
|
||||||
# yet (the callers copy a clean image per run). Human68k FAT12 is otherwise ordinary FAT12.
|
# file staged into the same SAVES/), add INTO it rather than creating a duplicate root entry the
|
||||||
|
# guest's SUB lookup would never resolve. Human68k FAT12 is otherwise ordinary FAT12.
|
||||||
dstem = dirname.upper()
|
dstem = dirname.upper()
|
||||||
if len(dstem) > 8:
|
if len(dstem) > 8:
|
||||||
raise ValueError(f"subdir '{dirname}' does not fit 8.3")
|
raise ValueError(f"subdir '{dirname}' does not fit 8.3")
|
||||||
cb = self.cluster_bytes
|
main11, ext10 = self.encode_name_ext(name)
|
||||||
need = max(1, (len(payload) + cb - 1) // cb)
|
_, _, de = self.find(dirname)
|
||||||
|
if de is not None and (de[11] & ATTR_DIR):
|
||||||
|
head = self._alloc_write_payload(payload)
|
||||||
|
entry = self._dir_entry(main11, ATTR_ARCHIVE, head, len(payload), ext10)
|
||||||
|
self._put_entry_in_dir(struct.unpack("<H", de[26:28])[0], entry)
|
||||||
|
return
|
||||||
|
# Fresh subdirectory: reserve its cluster first so the payload allocation cannot reuse it, then
|
||||||
|
# lay down '.', '..', and the file entry.
|
||||||
free = self.free_clusters()
|
free = self.free_clusters()
|
||||||
if len(free) < need + 1:
|
if not free:
|
||||||
raise OSError(f"{self.path}: need {need + 1} clusters, {len(free)} free")
|
raise OSError(f"{self.path}: no free cluster for subdirectory")
|
||||||
dir_cluster = free[0]
|
dir_cluster = free[0]
|
||||||
data_chain = free[1:1 + need]
|
self.fat_set(dir_cluster, 0xFFF)
|
||||||
# File data clusters.
|
head = self._alloc_write_payload(payload)
|
||||||
for idx, cluster in enumerate(data_chain):
|
cb = self.cluster_bytes
|
||||||
off = self.cluster_offset(cluster)
|
|
||||||
chunk = payload[idx * cb:(idx + 1) * cb]
|
|
||||||
self.data[off:off + len(chunk)] = chunk
|
|
||||||
if len(chunk) < cb:
|
|
||||||
self.data[off + len(chunk):off + cb] = bytes(cb - len(chunk))
|
|
||||||
self.fat_set(cluster, 0xFFF if idx == need - 1 else data_chain[idx + 1])
|
|
||||||
# The subdirectory's own cluster: '.', '..', then the file entry, rest free.
|
|
||||||
doff = self.cluster_offset(dir_cluster)
|
doff = self.cluster_offset(dir_cluster)
|
||||||
self.data[doff:doff + cb] = bytes(cb)
|
self.data[doff:doff + cb] = bytes(cb)
|
||||||
self.data[doff:doff + 32] = self._dir_entry(b". ", ATTR_DIR, dir_cluster, 0)
|
self.data[doff:doff + 32] = self._dir_entry(b". ", ATTR_DIR, dir_cluster, 0)
|
||||||
self.data[doff + 32:doff + 64] = self._dir_entry(b".. ", ATTR_DIR, 0, 0)
|
self.data[doff + 32:doff + 64] = self._dir_entry(b".. ", ATTR_DIR, 0, 0)
|
||||||
self.data[doff + 64:doff + 96] = self._dir_entry(
|
self.data[doff + 64:doff + 96] = self._dir_entry(main11, ATTR_ARCHIVE, head, len(payload), ext10)
|
||||||
self.encode_name(name), ATTR_ARCHIVE, data_chain[0], len(payload))
|
|
||||||
self.fat_set(dir_cluster, 0xFFF)
|
|
||||||
# Root entry pointing at the subdirectory.
|
|
||||||
slot = None
|
slot = None
|
||||||
for _, off, e in self._entries():
|
for _, off, e in self._entries():
|
||||||
if e[0] in (FREE_MARKER, DELETED_MARKER):
|
if e[0] in (FREE_MARKER, DELETED_MARKER):
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue