Files
rebbarb/gc_bba_test/source/netboot.c
T
Roflin 1327f82719 netboot: detect being lapped by the ring writer; quiet-link guidance
The alignment bug is FIXED -- hardware now reports "frame 1 (342 B) etype=0800
at 12: FF FF FF FF FF FF 00 2B", i.e. broadcast dst at 0, host MAC at 6,
ethertype at 12.  Perfectly aligned, correctly received, correctly skipped as
not ours.

So 00 00 00 01 was never a dropped write.  It is frame data being read as a
descriptor, because the ring HAS NO OVERFLOW PROTECTION: nothing in the
gateware consumes RRP, so RXFrameAssembler keeps writing and laps the reader
(the real BBA has RBFI for exactly this).  The poll loop costs ~5 EXI
transactions per frame (~200 us) while a 100 Mbit link delivers a 342-byte
frame every ~30 us, and the ring is 15 pages -- roughly 3 to 11 frames.  Under
continuous traffic pages are reused before they are read.  That is also why it
"worked once out of three tries": it is a race against the host's background
traffic.

ring_next() now returns -4 when the writer is more than two thirds of the ring
ahead, and the caller resyncs and continues instead of parsing bytes that
belong to a different frame.  The timeout reports seen / bad-descriptor /
lapped counts so the three cases stay distinguishable.

Detection is not prevention, so the app and the README both now say to quiet
the link first with gc-testlink.sh, which sets a static address and takes the
interface out of NetworkManager -- stopping the DHCP client whose 342-byte
broadcasts are the main offender.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 12:13:29 +02:00

670 lines
22 KiB
C

#include "netboot.h"
#include "bba.h"
#include <stdio.h>
#include <string.h>
#include <malloc.h>
#include <unistd.h>
#include <ogc/lwp_watchdog.h>
#include <ogc/cache.h>
#include <fat.h>
#include <sdcard/gcsd.h>
/* ── frame layout ────────────────────────────────────────────────────────
* 0 dst MAC (6)
* 6 src MAC (6)
* 12 ethertype (2) = NB_ETHERTYPE
* 14 magic (4) = "GCUP"
* 18 opcode (1)
* 19 reserved (1)
* 20 seq (2, big endian)
* 22 nblocks (2, big endian)
* 24 len (2, big endian)
* 26 data (len)
*/
#define NB_HDR 26u
#define NB_MIN 60u /* ethernet minimum, padded */
static u8 s_our_mac[6];
static u8 s_peer_mac[6];
/* Last descriptor ring_next() looked at, kept for the error path: when a
* length is rejected, the four raw bytes are the whole diagnosis. */
static u8 s_last_desc[4];
static u8 s_last_dump[32];
static u32 s_last_page;
static u16 rd16(const u8 *p) { return ((u16)p[0] << 8) | p[1]; }
static void wr16(u8 *p, u16 v) { p[0] = (u8)(v >> 8); p[1] = (u8)v; }
/* ── RX: pull one frame out of the SPRAM ring ────────────────────────────
* The ring is pages 1-15 with a 4-byte descriptor at each page start whose
* length field INCLUDES the descriptor. RRP is ours to advance; the
* gateware does not act on it. */
/* Pages the writer is ahead of us, over the 15-page ring. */
static u32 ring_lag(u32 rpage, u32 wpage)
{
return (wpage >= rpage) ? (wpage - rpage) : (wpage + 15u - rpage);
}
static int ring_next(u8 *buf, u32 cap)
{
u8 rp[2], wp[2], hdr[4];
u32 rpage, wpage, dlen, flen, addr;
if (bba_read(REG_RRP_LO, rp, 2) < 0) return -1;
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
rpage = ((u32)rp[1] << 8) | rp[0];
wpage = ((u32)wp[1] << 8) | wp[0];
if (rpage == wpage) return 0; /* empty */
if (rpage < 1 || rpage > 15) return -2; /* pointer corrupt */
/* THE RING HAS NO OVERFLOW PROTECTION. Nothing in the gateware consumes
* RRP, so RXFrameAssembler keeps writing and laps us: the real BBA has
* RBFI for this and we do not. Reading from a page the writer has
* already reused yields a "descriptor" that is really frame data -- which
* is where 00 00 00 01 came from, not a dropped write.
*
* Our poll loop costs ~5 EXI transactions per frame (~200 us) while a
* 100 Mbit link delivers a 342-byte frame every ~30 us, and the ring is
* only 15 pages, so under continuous traffic we lose that race. Bail out
* once the writer is more than two thirds of the ring ahead: the data we
* would read is not trustworthy, and resyncing is strictly better than
* parsing someone else's bytes. */
if (ring_lag(rpage, wpage) > 10) return -4;
addr = rpage << 8;
if (bba_stream((u16)addr, hdr, 4) < 0) return -1;
memcpy(s_last_desc, hdr, 4);
s_last_page = rpage;
/* Keep the head of the page for the error path. Guessing at a 4-byte
* descriptor has already cost several round trips; 32 bytes shows
* whether an ethernet header is present and AT WHAT OFFSET, which
* settles "bad length" versus "reading the wrong place" outright. */
if (bba_stream((u16)addr, s_last_dump, sizeof(s_last_dump)) < 0)
return -1;
dlen = ((u32)hdr[2] << 8) | hdr[3];
if (dlen <= 4 || dlen > 1518 + 4) return -3; /* descriptor implausible */
flen = dlen - 4;
if (flen > cap) flen = cap;
/* Descriptor + frame are contiguous from the page start. Read both and
* drop the descriptor rather than streaming from a mid-page address, so
* one DMA read covers it. */
{
static u8 tmp[1536 + 4];
u32 want = dlen;
if (want > sizeof(tmp)) want = sizeof(tmp);
if (bba_stream((u16)addr, tmp, want) < 0) return -1;
memcpy(buf, tmp + 4, flen);
}
/* Advance RRP by the pages this frame occupied, wrapping 1..15. */
{
u32 pages = (dlen + 255) >> 8;
u32 next = ((rpage - 1 + pages) % 15) + 1;
if (bba_w8(REG_RRP_LO, (u8)next) < 0) return -1;
}
return (int)flen;
}
static int send_ack(u16 seq)
{
u8 f[NB_MIN];
memset(f, 0, sizeof(f));
memcpy(f + 0, s_peer_mac, 6);
memcpy(f + 6, s_our_mac, 6);
wr16(f + 12, NB_ETHERTYPE);
f[14] = 'G'; f[15] = 'C'; f[16] = 'U'; f[17] = 'P';
f[18] = NB_OP_ACK;
wr16(f + 20, seq);
wr16(f + 22, 0);
wr16(f + 24, 0);
return bba_tx_bytes(f, sizeof(f));
}
/* Destination path carried in the DONE frame, so the host decides where the
* build lands rather than it being baked in here. */
static char s_dest[64] = "/gc_bba_test.dol";
const char *netboot_dest_path(void) { return s_dest; }
int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg))
{
static u8 frame[1536];
u8 *out = (u8 *)dst;
u32 got = 0, nblocks = 0, bad = 0, seen = 0, lapped = 0;
u16 expect = 0;
char line[80];
u64 t0;
int have_peer = 0;
/* Bring the device up deterministically rather than hoping someone else
* already did. Pressing Z straight after launch is the normal case, and
* then NOTHING has issued the NCRA reset that triggers the W5100's own
* init -- MACRAW is not running, no frames are stored, and this would sit
* in "timeout: no frames" for a reason that has nothing to do with the
* transfer. Swiss may have initialised it, or an earlier test may have,
* or neither; a reset here makes all three cases identical.
*
* The reset also clears the SPRAM ring pointers, which is what we want:
* a clean ring with RRP == RWP == page 1, so the first frame we see is
* genuinely the host's first block. */
if (status) status("resetting the adapter...");
if (bba_w8(REG_NCRA, NCRA_RESET) < 0) return -1;
{
int i, v = -1;
for (i = 0; i < 50; i++) {
v = bba_r8(REG_NCRA);
if (v >= 0 && !(v & NCRA_RESET)) break;
usleep(5000);
}
if (i == 50) {
if (status) status("NCRA reset never self-cleared");
return -1;
}
/* The W5100 re-inits behind that: RSTn pulse plus a 60.3 ms settle,
* done twice. Polling it sooner reads all-zeros and looks dead. */
usleep(400000);
}
/* Start receive. Without the SR bit the assembler never writes the
* ring, so frames arrive at the W5100 and go nowhere. */
if (bba_w8(REG_NCRA, NCRA_SR) < 0) return -1;
/* Establish a KNOWN ring state instead of trusting what we inherited.
* Swiss initialises the BBA before launching us and writes RRP itself,
* and an NCRA reset does not restore the register file's copy of either
* pointer -- their init values only apply at FPGA configuration. So
* RRP can legitimately be anything, including 0, which is outside the
* ring (pages 1-15) and used to abort the whole transfer.
*
* Setting RRP = RWP declares the ring empty and consistent, whatever it
* was, so the first frame we read is genuinely the host's first block. */
{
u8 wp[2];
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
if (wp[0] < 1 || wp[0] > 15) {
/* RWP is the gateware's own pointer, so this should not
* happen -- say so loudly rather than papering over it. */
if (status) { snprintf(line, sizeof(line),
"RWP=%u outside ring pages 1-15", wp[0]);
status(line); }
return -5;
}
if (bba_w8(REG_RRP_LO, wp[0]) < 0) return -1;
if (status) { snprintf(line, sizeof(line),
"ring synced at page %u", wp[0]); status(line); }
}
/* PAR0-5 survives the reset (it is the gateware's power-on MAC), and it
* is read AFTER the reset so the source address we put in ACKs is the
* one the adapter is actually filtering on. */
if (bba_read(REG_PAR0, s_our_mac, 6) < 0) return -1;
if (status) {
status("waiting for host (send-dol.py)...");
status("NOTE: quiet the link first, or other traffic laps the ring:");
status(" sudo ./scripts/gc-testlink.sh up <iface>");
}
t0 = gettime();
for (;;) {
int n = ring_next(frame, sizeof(frame));
if (n == -4) {
/* Lapped. Skip to the writer and carry on; with stop-and-wait
* the host simply retransmits the block we just threw away. */
lapped++;
if (lapped <= 3 && status)
status("ring lapped (too much other traffic) - resyncing");
{
u8 wp[2];
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
if (wp[0] >= 1 && wp[0] <= 15)
bba_w8(REG_RRP_LO, wp[0]);
}
continue;
}
if (n == -3) {
/* A descriptor we cannot make sense of must NOT kill the
* transfer. The ring collects EVERYTHING on the wire --
* mDNS, IPv6 RAs, ARP from the host -- so pressing Z on a
* live network hits stray frames long before the sender
* starts. Log the first few, resync, and carry on. */
bad++;
if (bad <= 3 && status) {
int i;
snprintf(line, sizeof(line),
"skip bad descriptor at page %u: %02X %02X %02X %02X",
(unsigned)s_last_page, s_last_desc[0],
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
status(line);
for (i = 0; i < 32; i += 16) {
snprintf(line, sizeof(line),
" +%02X %02X %02X %02X %02X %02X %02X %02X %02X "
"%02X %02X %02X %02X %02X %02X %02X %02X", i,
s_last_dump[i+0], s_last_dump[i+1], s_last_dump[i+2],
s_last_dump[i+3], s_last_dump[i+4], s_last_dump[i+5],
s_last_dump[i+6], s_last_dump[i+7], s_last_dump[i+8],
s_last_dump[i+9], s_last_dump[i+10], s_last_dump[i+11],
s_last_dump[i+12], s_last_dump[i+13], s_last_dump[i+14],
s_last_dump[i+15]);
status(line);
}
}
if (bad > 200) {
if (status) status("too many bad descriptors - giving up");
return -2;
}
/* Resync to the gateware's own pointer and keep waiting. */
{
u8 wp[2];
if (bba_read(REG_RWP_LO, wp, 2) < 0) return -1;
if (wp[0] >= 1 && wp[0] <= 15)
bba_w8(REG_RRP_LO, wp[0]);
}
continue;
}
if (n < 0) {
/* Report the pointers, not just the code: -1 is an EXI access
* failure, -2 an RRP outside the ring, -3 a descriptor whose
* length is implausible. Which one it is, and what the
* pointers read, is the whole diagnosis. */
u8 rp[2] = {0xFF, 0xFF}, wp[2] = {0xFF, 0xFF};
bba_read(REG_RRP_LO, rp, 2);
bba_read(REG_RWP_LO, wp, 2);
if (status) {
snprintf(line, sizeof(line),
"ring error %d (RRP=%u RWP=%u) %s", n, rp[0], wp[0],
n == -1 ? "EXI access failed" :
n == -2 ? "RRP outside pages 1-15" :
"descriptor length implausible");
status(line);
snprintf(line, sizeof(line),
" page %u descriptor = %02X %02X %02X %02X",
(unsigned)s_last_page, s_last_desc[0],
s_last_desc[1], s_last_desc[2], s_last_desc[3]);
status(line);
}
return -2;
}
if (n == 0) {
/* 30 s with nothing at all, or 5 s mid-transfer, is a bail-out
* rather than a hang -- a wedged update must not require a
* console power cycle. */
u32 idle = (u32)ticks_to_millisecs(diff_ticks(t0, gettime()));
if (idle > (got ? 5000u : 30000u)) {
if (status) {
snprintf(line, sizeof(line),
"%s (%u seen, %u bad desc, %u lapped)",
got ? "timeout mid-transfer" : "timeout: no match",
(unsigned)seen, (unsigned)bad, (unsigned)lapped);
status(line);
}
return -3;
}
continue;
}
seen++;
if ((u32)n < NB_HDR) continue;
if (rd16(frame + 12) != NB_ETHERTYPE) {
/* Dump the first non-matching frame. If the sender is running
* and nothing matches, the bytes show whether the body is
* offset -- the ethertype would appear at 11 or 13 instead of
* 12. */
if (seen == 1 && status) {
snprintf(line, sizeof(line),
"frame 1 (%d B) etype=%04X at 12; "
"%02X %02X %02X %02X %02X %02X %02X %02X",
n, rd16(frame + 12), frame[0], frame[1], frame[2],
frame[3], frame[4], frame[5], frame[6], frame[7]);
status(line);
}
continue;
}
if (frame[14] != 'G' || frame[15] != 'C' ||
frame[16] != 'U' || frame[17] != 'P') continue;
if (!have_peer) { memcpy(s_peer_mac, frame + 6, 6); have_peer = 1; }
t0 = gettime();
if (frame[18] == NB_OP_DONE) {
/* DONE may carry the destination path as its payload. */
u16 plen = rd16(frame + 24);
if (plen && plen < sizeof(s_dest) && NB_HDR + plen <= (u32)n) {
memcpy(s_dest, frame + NB_HDR, plen);
s_dest[plen] = 0;
}
if (status) { snprintf(line, sizeof(line),
"received %u bytes in %u blocks",
(unsigned)got, (unsigned)nblocks);
status(line); }
return (int)got;
}
if (frame[18] != NB_OP_DATA) continue;
{
u16 seq = rd16(frame + 20);
u16 len = rd16(frame + 24);
nblocks = rd16(frame + 22);
if (len > NB_BLOCK_MAX || NB_HDR + len > (u32)n) continue;
if (seq == expect) {
if (got + len > maxlen) {
if (status) status("image larger than buffer");
return -4;
}
memcpy(out + got, frame + NB_HDR, len);
got += len;
expect++;
if ((expect & 0x1F) == 0 && status) {
snprintf(line, sizeof(line), "block %u/%u (%u KB)",
(unsigned)expect, (unsigned)nblocks,
(unsigned)(got >> 10));
status(line);
}
}
/* ACK even a duplicate: it means our previous ACK was lost, and
* re-ACKing is what lets the host move on. */
send_ack(seq);
}
}
}
/* ── DOL loader ──────────────────────────────────────────────────────────
* 7 text + 11 data sections, each with a file offset, load address and size,
* then BSS and the entry point. All big endian. */
typedef struct {
u32 text_off[7];
u32 data_off[11];
u32 text_addr[7];
u32 data_addr[11];
u32 text_len[7];
u32 data_len[11];
u32 bss_addr;
u32 bss_len;
u32 entry;
u32 pad[7];
} dolhdr_t;
/* Reserved region at the top of MEM1:
*
* [ staged image .............. ][ stub ][ descriptor ][ new stack ]
* ^ s_stage ^ s_stub
*
* Nothing here may overlap any DOL section, and netboot_launch() checks that
* before it jumps rather than trusting it. */
#define NB_STAGE_MAX (6u << 20) /* generous: Swiss is ~2-3 MB */
#define NB_TAIL (64u << 10) /* stub + descriptor + stack */
extern void nb_stub(void *desc);
extern void nb_stub_end(void);
static u8 *s_stage;
static u8 *s_tail;
void *netboot_reserve(u32 *out_max)
{
u32 hi = (u32)SYS_GetArena1Hi();
u32 base = (hi - (NB_STAGE_MAX + NB_TAIL)) & ~31u;
if (base <= (u32)SYS_GetArena1Lo()) return NULL;
s_stage = (u8 *)base;
s_tail = (u8 *)(base + NB_STAGE_MAX);
/* Tell libogc this is not its memory any more. */
SYS_SetArena1Hi(s_stage);
if (out_max) *out_max = NB_STAGE_MAX;
return s_stage;
}
static int overlaps(u32 a, u32 alen, u32 b, u32 blen)
{
if (!alen || !blen) return 0;
return a < b + blen && b < a + alen;
}
int netboot_launch(const void *dol)
{
const dolhdr_t *h = (const dolhdr_t *)dol;
const u8 *base = (const u8 *)dol;
u32 reserved = (u32)s_stage;
u32 reserved_len = NB_STAGE_MAX + NB_TAIL;
u32 stub_len = (u32)((const u8 *)nb_stub_end - (const u8 *)nb_stub);
u32 *desc;
void (*stub)(void *);
int i, nsec = 0;
if (!s_stage) return -1;
/* Refuse a mis-parsed or truncated header rather than jumping into it:
* a bad jump hangs the console hard enough to need a power cycle. */
if ((h->entry >> 24) != 0x80 || h->text_len[0] == 0 ||
(h->text_addr[0] >> 24) != 0x80)
return -2;
/* Build the copy table in the reserved tail, where the image cannot
* reach it. While doing so, verify no section lands on the reserved
* region -- that would clobber the staged image, the stub we are about
* to run from, or this very table. */
desc = (u32 *)(s_tail + 0x1000);
for (i = 0; i < 7; i++) {
if (!h->text_len[i]) continue;
if (overlaps(h->text_addr[i], h->text_len[i], reserved, reserved_len))
return -3;
desc[5 + nsec * 3 + 0] = (u32)(base + h->text_off[i]);
desc[5 + nsec * 3 + 1] = h->text_addr[i];
desc[5 + nsec * 3 + 2] = h->text_len[i];
nsec++;
}
for (i = 0; i < 11; i++) {
if (!h->data_len[i]) continue;
if (overlaps(h->data_addr[i], h->data_len[i], reserved, reserved_len))
return -3;
desc[5 + nsec * 3 + 0] = (u32)(base + h->data_off[i]);
desc[5 + nsec * 3 + 1] = h->data_addr[i];
desc[5 + nsec * 3 + 2] = h->data_len[i];
nsec++;
}
if (h->bss_len &&
overlaps(h->bss_addr, h->bss_len, reserved, reserved_len))
return -3;
desc[0] = h->entry;
/* Hand the new image a stack inside the reserved tail, 64-byte aligned
* and clear of the descriptor. */
desc[1] = ((reserved + reserved_len) - 64) & ~63u;
desc[2] = (u32)nsec;
desc[3] = h->bss_addr;
desc[4] = h->bss_len;
/* Relocate the copier out of harm's way and make it executable there. */
stub = (void (*)(void *))s_tail;
memcpy(s_tail, (const void *)nb_stub, stub_len);
DCFlushRange(s_tail, NB_TAIL);
ICInvalidateRange(s_tail, NB_TAIL);
/* No interrupts from here on: we are about to overwrite the memory this
* program is running from, and the scheduler must not preempt us into
* code that no longer exists. */
IRQ_Disable();
stub(desc);
return 0; /* not reached */
}
/* ── SD card: save the new build, then boot a known-good image ───────────
* The update deliberately does NOT launch what it just received. It writes
* the image to the card and then boots /boot.dol (Swiss), so a broken test
* build is recoverable: Swiss comes up regardless and can load the previous
* one. The only image we ever jump into is one that was already known good. */
static int s_sd_mounted;
static int sd_mount(void (*status)(const char *msg))
{
/* Try every interface a GameCube might have. Auto-detecting beats
* asking: SD Gecko can be in either memory-card slot, and SD2SP2 is on
* its own EXI channel. */
static const struct { const char *name; const DISC_INTERFACE *io; } ifs[] = {
{ "SD2SP2", &__io_gcsd2 },
{ "SD Gecko slot B", &__io_gcsdb },
{ "SD Gecko slot A", &__io_gcsda },
};
unsigned i;
char line[64];
if (s_sd_mounted) return 0;
for (i = 0; i < sizeof(ifs) / sizeof(ifs[0]); i++) {
if (fatMountSimple("sd", ifs[i].io)) {
if (status) {
snprintf(line, sizeof(line), "SD mounted (%s)", ifs[i].name);
status(line);
}
s_sd_mounted = 1;
return 0;
}
}
if (status) status("no SD card found (SD2SP2 or SD Gecko)");
return -1;
}
/* Build "<base>.tmp" and "<base>.prev.dol" from "<base>.dol".
*
* The temp file deliberately does NOT end in .dol: Swiss lists .dol files, and
* a half-written image that Swiss offers to boot is exactly what we are trying
* to prevent. The backup DOES end in .dol, because booting it from Swiss is
* the whole point of keeping it. */
static void derive_names(const char *path, char *tmp, size_t ntmp,
char *prev, size_t nprev)
{
char base[80];
size_t n = strlen(path);
if (n >= sizeof(base)) n = sizeof(base) - 1;
memcpy(base, path, n);
base[n] = 0;
if (n > 4 && strcasecmp(base + n - 4, ".dol") == 0)
base[n - 4] = 0;
snprintf(tmp, ntmp, "sd:%s.tmp", base);
snprintf(prev, nprev, "sd:%s.prev.dol", base);
}
int netboot_sd_write(const char *path, const void *buf, u32 len,
void (*status)(const char *msg))
{
char final[96], tmp[112], prev[112], line[160];
FILE *f;
u32 done;
int rc = 0;
if (sd_mount(status) < 0) return -1;
snprintf(final, sizeof(final), "sd:%s", path);
derive_names(path, tmp, sizeof(tmp), prev, sizeof(prev));
/* Write to a TEMP file and verify it BEFORE touching the existing
* build. Ordering matters: if the transfer, the write or the verify
* fails, the copy you are running from is still on the card untouched,
* so you can always try the update again. Only once the new image is
* known good do we rotate. */
if (status) { snprintf(line, sizeof(line), "writing %s", tmp); status(line); }
f = fopen(tmp, "wb");
if (!f) { if (status) status("open for write failed"); return -2; }
done = (u32)fwrite(buf, 1, len, f);
fclose(f);
if (done != len) {
remove(tmp);
if (status) status("short write - existing build left alone");
return -3;
}
/* Read it back. A half-written .dol that still parses is the failure
* this design exists to avoid, and the card is the one component here
* with no other integrity check. */
if (status) status("verifying...");
f = fopen(tmp, "rb");
if (!f) { remove(tmp); if (status) status("reopen for verify failed"); return -4; }
{
static u8 chunk[4096];
const u8 *src = (const u8 *)buf;
u32 off = 0;
while (off < len) {
u32 want = len - off;
if (want > sizeof(chunk)) want = sizeof(chunk);
if (fread(chunk, 1, want, f) != want) { rc = -5; break; }
if (memcmp(chunk, src + off, want) != 0) { rc = -6; break; }
off += want;
}
}
fclose(f);
if (rc < 0) {
remove(tmp);
if (status) status("VERIFY FAILED - existing build left alone");
return rc;
}
/* Rotate: keep exactly one generation back. remove()/rename() of a
* file that is not there is fine and ignored -- that is just the first
* update on a fresh card. */
remove(prev);
rename(final, prev);
if (rename(tmp, final) != 0) {
/* Should not happen, but if it does the old build is sitting in
* .prev.dol and is still bootable from Swiss -- say so rather
* than leaving the user to guess. */
if (status) {
snprintf(line, sizeof(line),
"rename failed; previous build is at %s", prev);
status(line);
}
return -7;
}
if (status) {
snprintf(line, sizeof(line), "wrote %u bytes; previous build kept as %s",
(unsigned)len, prev);
status(line);
}
return 0;
}
int netboot_boot_file(const char *path, void *dst, u32 maxlen,
void (*status)(const char *msg))
{
char full[96], line[128];
FILE *f;
long sz;
u32 got;
if (sd_mount(status) < 0) return -1;
snprintf(full, sizeof(full), "sd:%s", path);
f = fopen(full, "rb");
if (!f) {
if (status) { snprintf(line, sizeof(line), "%s not found", full);
status(line); }
return -2;
}
fseek(f, 0, SEEK_END);
sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= 0 || (u32)sz > maxlen) {
fclose(f);
if (status) status("boot image size implausible");
return -3;
}
got = (u32)fread(dst, 1, (size_t)sz, f);
fclose(f);
if (got != (u32)sz) {
if (status) status("short read of boot image");
return -4;
}
if (status) { snprintf(line, sizeof(line), "booting %s (%u bytes)",
full, (unsigned)got); status(line); }
/* Unmount before we disable interrupts and jump: leaving the card
* mounted hands the next image a filesystem that libfat still thinks it
* owns, on an EXI channel it is about to reinitialise. */
fatUnmount("sd");
s_sd_mounted = 0;
return netboot_launch(dst);
}