diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..d086652 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,83 @@ +name: CI + +on: + push: + pull_request: + +jobs: + nro: + name: Build NRO + runs-on: ubuntu-latest + container: + image: devkitpro/devkita64:latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure + run: cmake --preset switch + + - name: Build + run: cmake --build build -j$(nproc) + + - uses: actions/upload-artifact@v4 + with: + name: NXST-${{ github.sha }} + path: build/NXST.nro + + release: + name: Upload release asset + runs-on: ubuntu-latest + needs: nro + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - uses: actions/download-artifact@v4 + with: + name: NXST-${{ github.sha }} + + - name: Create Gitea release and upload NRO + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + TAG: ${{ github.ref_name }} + API: ${{ github.server_url }}/api/v1 + REPO: ${{ github.repository }} + run: | + release_id=$(curl -sf -X POST "$API/repos/$REPO/releases" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\"}" \ + | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + + curl -sf -X POST "$API/repos/$REPO/releases/$release_id/assets?name=NXST.nro" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @NXST.nro + + format: + name: Format check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install clang-format + run: sudo apt-get install -y clang-format + + - name: Check formatting + run: | + find src include \( -name '*.cpp' -o -name '*.hpp' \) \ + | xargs clang-format --dry-run --Werror + + layering: + name: Layering check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: UI must not include net/sys headers + run: | + ! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..39eb0a0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,72 @@ +name: CI + +on: + push: + pull_request: + +jobs: + nro: + name: Build NRO + runs-on: ubuntu-latest + container: + image: devkitpro/devkita64:latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure + run: cmake --preset switch + + - name: Build + run: cmake --build build -j$(nproc) + + - uses: actions/upload-artifact@v4 + with: + name: NXST-${{ github.sha }} + path: build/NXST.nro + + release: + name: Upload release asset + runs-on: ubuntu-latest + needs: nro + if: startsWith(github.ref, 'refs/tags/v') + + permissions: + contents: write + + steps: + - uses: actions/download-artifact@v4 + with: + name: NXST-${{ github.sha }} + + - uses: softprops/action-gh-release@v2 + with: + files: NXST.nro + + format: + name: Format check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install clang-format + run: sudo apt-get install -y clang-format + + - name: Check formatting + run: | + find src include \( -name '*.cpp' -o -name '*.hpp' \) \ + | xargs clang-format --dry-run --Werror + + layering: + name: Layering check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: UI must not include net/sys headers + run: | + ! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e12dae0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to NXST follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Version numbers are calendar-based (`MM.DD.YYYY`) until the protocol stabilises. + +--- + +## [Unreleased] + +### Changed +- Full architectural refactor (8 phases): + - Layered directory structure: `src/` + `include/nxst/` mirroring domain / infra / service / ui + - CMake build system replacing Makefile + - `nxst::TransferService` — all network state and threads moved out of UI layer + - `nxst::Result` tagged-union type replacing `std::tuple` + - RAII handles: `FsFileSystemHandle`, `FileHandle` + - Logger rewritten with `nxst::log` namespace and compile-time format checking + - snake_case filenames and identifiers throughout + +### Fixed +- `mkdir(path, 777)` was decimal (octal `01411`) — corrected to `0777` +- Logger format args were silently dropped (double-substitution bug) +- `new u8[]` / `malloc` in IO paths replaced with `std::vector` + +--- + +## [04.26.2026] + +### Added +- Initial public release: save transfer between two Nintendo Switch consoles over local Wi-Fi +- Multicast discovery (UDP `239.0.0.1:8081`) + TCP file transfer (`:8080`) +- Send and Receive modes with progress overlay and cancel support +- Automatic save backup before transfer +- Restore replaces live save data and commits to the save device diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..7a816f8 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "switch", + "displayName": "Nintendo Switch", + "toolchainFile": "$env{DEVKITPRO}/cmake/Switch.cmake", + "binaryDir": "${sourceDir}/build" + } + ], + "buildPresets": [ + { + "name": "switch", + "configurePreset": "switch" + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fa6ac14 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + For the full license text see . + +------------------------------------------------------------------------------- + +NXST includes code derived from Checkpoint by Bernardo Giordano / FlagBrew, +licensed under GPLv3. The derived files are: + + src/infra/fs/io.cpp + src/domain/account.cpp + src/domain/title.cpp + include/nxst/infra/fs/filesystem.hpp + src/infra/fs/filesystem.cpp + src/infra/fs/directory.cpp + +All original NXST code is also released under the GNU General Public +License v3 or later, to satisfy the license inheritance requirement. + +Original Checkpoint repository: https://github.com/BernardoGiordano/Checkpoint diff --git a/Makefile b/Makefile deleted file mode 100644 index 2f23ac4..0000000 --- a/Makefile +++ /dev/null @@ -1,204 +0,0 @@ -#--------------------------------------------------------------------------------- -.SUFFIXES: -#--------------------------------------------------------------------------------- - -ifeq ($(strip $(DEVKITPRO)),) -$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") -endif - -TOPDIR ?= $(CURDIR) -include $(DEVKITPRO)/libnx/switch_rules - -#--------------------------------------------------------------------------------- -# TARGET is the name of the output -# BUILD is the directory where object files & intermediate files will be placed -# SOURCES is a list of directories containing source code -# DATA is a list of directories containing data files -# INCLUDES is a list of directories containing header files -# EXEFS_SRC is the optional input directory containing data copied into exefs, if anything this normally should only contain "main.npdm". -# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) -# -# NO_ICON: if set to anything, do not use icon. -# NO_NACP: if set to anything, no .nacp file is generated. -# APP_TITLE is the name of the app stored in the .nacp file (Optional) -# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) -# APP_VERSION is the version of the app stored in the .nacp file (Optional) -# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) -# ICON is the filename of the icon (.jpg), relative to the project folder. -# If not set, it attempts to use one of the following (in this order): -# - .jpg -# - icon.jpg -# - /default_icon.jpg -#--------------------------------------------------------------------------------- -TARGET := NXST -BUILD := build -SOURCES := src/app src/domain src/infra/net src/infra/fs src/infra/sys src/service src/ui lib/Plutonium/source -INCLUDES := include lib/Plutonium/include -EXEFS_SRC := exefs_src -APP_TITLE := NXST -APP_AUTHOR := DragonSpirit -APP_VERSION := 04.26.2026 -ICON := icon.png - -#--------------------------------------------------------------------------------- -# options for code generation -#--------------------------------------------------------------------------------- -ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE - -CFLAGS += -g -O2 -ffunction-sections $(ARCH) $(DEFINES) - -CFLAGS += $(INCLUDE) -D__SWITCH__ -D_GNU_SOURCE=1 - -CXXFLAGS:= $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 -g - -ASFLAGS := -g $(ARCH) -LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) - -PKGCONF := $(DEVKITPRO)/portlibs/switch/bin/aarch64-none-elf-pkg-config -PKG_LIBS := SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer freetype2 harfbuzz minizip libpng libjpeg libwebp glesv2 egl glapi zlib -LIBS := -lpu $(shell $(PKGCONF) --libs $(PKG_LIBS)) -ldrm_nouveau -lharfbuzz -lfreetype -lz - -#--------------------------------------------------------------------------------- -# list of directories containing libraries, this must be the top level containing -# include and lib -#--------------------------------------------------------------------------------- -LIBDIRS := $(PORTLIBS) $(LIBNX) $(CURDIR)/lib/Plutonium - - -#--------------------------------------------------------------------------------- -# no real need to edit anything past this point unless you need to add additional -# rules for different file extensions -#--------------------------------------------------------------------------------- -ifneq ($(BUILD),$(notdir $(CURDIR))) -#--------------------------------------------------------------------------------- - -export OUTPUT := $(CURDIR)/$(TARGET) -export TOPDIR := $(CURDIR) - -export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ - $(foreach dir,$(DATA),$(CURDIR)/$(dir)) - -export DEPSDIR := $(CURDIR)/$(BUILD) - -CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) -CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) -SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) -BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) - -#--------------------------------------------------------------------------------- -# use CXX for linking C++ projects, CC for standard C -#--------------------------------------------------------------------------------- -ifeq ($(strip $(CPPFILES)),) -#--------------------------------------------------------------------------------- - export LD := $(CC) -#--------------------------------------------------------------------------------- -else -#--------------------------------------------------------------------------------- - export LD := $(CXX) -#--------------------------------------------------------------------------------- -endif -#--------------------------------------------------------------------------------- - -export OFILES_BIN := $(addsuffix .o,$(BINFILES)) -export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) -export OFILES := $(OFILES_BIN) $(OFILES_SRC) -export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) - -export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ - $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ - -I$(CURDIR)/$(BUILD) - -export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) - -export BUILD_EXEFS_SRC := $(TOPDIR)/$(EXEFS_SRC) - -ifeq ($(strip $(ICON)),) - icons := $(wildcard *.jpg) - ifneq (,$(findstring $(TARGET).jpg,$(icons))) - export APP_ICON := $(TOPDIR)/$(TARGET).jpg - else - ifneq (,$(findstring icon.jpg,$(icons))) - export APP_ICON := $(TOPDIR)/icon.jpg - endif - endif -else - export APP_ICON := $(TOPDIR)/$(ICON) -endif - -ifeq ($(strip $(NO_ICON)),) - export NROFLAGS += --icon=$(APP_ICON) -endif - -ifeq ($(strip $(NO_NACP)),) - export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp -endif - -ifneq ($(APP_TITLEID),) - export NACPFLAGS += --titleid=$(APP_TITLEID) -endif - -ifneq ($(ROMFS),) - export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) -endif - -.PHONY: $(BUILD) clean all - -#--------------------------------------------------------------------------------- -all: $(BUILD) - -$(BUILD): - @[ -d $@ ] || mkdir -p $@ - @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile - -#--------------------------------------------------------------------------------- -clean: - @echo clean ... - @rm -fr $(BUILD) $(TARGET).pfs0 $(TARGET).nso $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst - -cleanbuild: clean all - -#--------------------------------------------------------------------------------- -send: $(BUILD) - @nxlink $(TARGET).nro - -debug: $(BUILD) - @nxlink -s $(TARGET).nro - - -else -.PHONY: all - -DEPENDS := $(OFILES:.o=.d) - -#--------------------------------------------------------------------------------- -# main targets -#--------------------------------------------------------------------------------- -all : $(OUTPUT).pfs0 $(OUTPUT).nro - -$(OUTPUT).pfs0 : $(OUTPUT).nso - -$(OUTPUT).nso : $(OUTPUT).elf - -ifeq ($(strip $(NO_NACP)),) -$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp -else -$(OUTPUT).nro : $(OUTPUT).elf -endif - -$(OUTPUT).elf : $(OFILES) - -$(OFILES_SRC) : $(HFILES_BIN) - -#--------------------------------------------------------------------------------- -# you need a rule like this for each extension you use as binary data -#--------------------------------------------------------------------------------- -%.bin.o %_bin.h : %.bin -#--------------------------------------------------------------------------------- - @echo $(notdir $<) - @$(bin2o) - --include $(DEPENDS) - -#--------------------------------------------------------------------------------------- -endif -#--------------------------------------------------------------------------------------- diff --git a/PLAN.md b/PLAN.md index 5a02885..e93e5e7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -11,10 +11,10 @@ | 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain | | 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling | | 6 | `Result` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory | -| 7 | Documentation + license | ☐ Not started | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE | -| 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | +| 7 | Documentation + license | ✅ Done | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE | +| 8 | CI | ✅ Done | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | -**Active phase:** Phase 7 — Documentation + license. +**Active phase:** — All phases complete. **Last updated:** 2026-04-27. Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c33eea --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# NXST + +Transfer save data between two Nintendo Switch consoles over local Wi-Fi. + +Pick a game, pick a user. One Switch sends; the other receives. The sender backs up its own save first, so you can never lose data in transit. + +--- + +## Install + +1. Download `NXST.nro` from [Releases](../../releases). +2. Copy it to `/switch/NXST/NXST.nro` on your SD card. +3. Launch via hbmenu (hold R while starting any game, or Album). + +Both Switches must be on the same Wi-Fi network. No router configuration needed — discovery uses UDP multicast. + +--- + +## Usage + +**Sender** (the Switch whose save you want to copy): + +1. Open NXST → select a title → press **A** → **Transfer**. +2. Wait for "Waiting for receiver…" to change to "Transferring…". + +**Receiver** (the Switch that will receive the save): + +1. Open NXST → select the same title → press **A** → **Receive**. +2. Wait. The save is restored automatically when the transfer finishes. + +Press **B** on either side to cancel mid-transfer. + +Logs are written to `/switch/NXST/log.log`. + +--- + +## Build + +**Prerequisites:** [devkitPro](https://devkitpro.org/wiki/Getting_Started) with `switch-dev` and `switch-portlibs` packages, plus `cmake ≥ 3.20`. + +```bash +# Clone with submodules (Plutonium UI) +git clone --recurse-submodules https://github.com/your-username/NXST.git +cd NXST + +# Configure (once) +cmake --preset switch + +# Build +cmake --build build + +# Send to Switch via nxlink (Switch must be on same network, nxlink running) +cmake --build build --target send +``` + +Output: `build/NXST.nro` + +--- + +## Architecture + +See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the layer diagram, threading model, and key types. + +See [`docs/PROTOCOL.md`](docs/PROTOCOL.md) for the wire protocol (UDP multicast discovery + TCP file stream). + +``` +ui/ — TitlesLayout, UsersLayout, TransferOverlay, HeaderBar +service/ — TransferService (all network threads and state) +infra/net/ — Socket RAII, sendAll/recvAll +infra/fs/ — io::backup, io::restore, directory iterator, RAII handles +infra/sys/ — nxst::log (printf-checked, timestamped) +domain/ — Title, Account, Result, TransferState, protocol constants +``` + +--- + +## Credits + +- **[Plutonium](https://github.com/XorTroll/Plutonium)** — Switch UI framework by XorTroll +- **[Checkpoint](https://github.com/BernardoGiordano/Checkpoint)** — save management library by Bernardo Giordano / FlagBrew; several files in `src/infra/fs/` and `src/domain/` are derived from Checkpoint + +--- + +## License + +GPLv3 — see [`LICENSE`](LICENSE). + +NXST includes code derived from Checkpoint (GPLv3). All original NXST code is released under the same license to satisfy the GPL inheritance requirement. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..0d0f748 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,163 @@ +# NXST Architecture + +## Layer Diagram + +``` + ┌──────────────────────────────┐ + │ ui/ │ + │ TitlesLayout, UsersLayout │ + │ TransferOverlay, HeaderBar │ + └──────────────┬───────────────┘ + │ calls + ┌──────────────▼───────────────┐ + │ service/ │ + │ TransferService │ + └──────┬───────────────┬───────┘ + │ │ + ┌────────────▼──┐ ┌───────▼────────┐ + │ infra/net/ │ │ infra/fs/ │ + │ TransferService│ │ io, directory │ + │ (sender thread │ │ filesystem │ + │ recv threads) │ │ handles │ + └────────────┬──┘ └───────┬────────┘ + │ │ + ┌────────────▼───────────────▼────────┐ + │ domain/ │ + │ Title, Account, TransferState │ + │ Result, protocol constants │ + └─────────────────────────────────────┘ + │ + ┌────────────────────▼────────────────┐ + │ libnx, Plutonium, SDL2, devkitpro│ + └─────────────────────────────────────┘ +``` + +**Hard layering rules:** +- `domain/` depends on nothing in the project. +- `infra/` depends only on `domain/`. +- `service/` depends on `domain/` + `infra/`. +- `ui/` depends only on `service/` + `domain/` — must not include ``, ``, ``, or call `recv`/`send` directly. +- `app/` (MainApplication, main.cpp) wires everything together. + +--- + +## Key Types + +### `nxst::TransferService` (`include/nxst/service/transfer_service.hpp`) + +Owns all network state for both transfer directions. One instance lives in `MainApplication`. + +| Method | Description | +|--------|-------------| +| `startSend(index, uid)` | Backup save, discover receiver, stream files | +| `startReceive(index, uid, title_name)` | Listen for sender, receive files, restore save | +| `cancelSend()` / `cancelReceive()` | Interrupt in-flight transfer (socket shutdown) | +| `isSendDone()` / `isReceiveDone()` | Polled by UI render loop | +| `restoreSucceeded()` / `restoreError()` | Restore outcome after receive | + +**Threading model:** + +``` +UI thread (Plutonium event loop) + └─ startSend() + └─ senderEntry [pthread, detached] + ├─ findServer() — UDP multicast, 100 ms poll slices + ├─ io::backup() — creates local save backup + └─ TCP send loop + + └─ startReceive() + ├─ broadcastEntry [pthread, detached] — UDP multicast listener + └─ acceptEntry [pthread, detached] + ├─ TCP receive loop + └─ io::restore() — mounts save, clears, copies, commits +``` + +Cancellation: `cancelSend()` / `cancelReceive()` call `shutdown(SHUT_RDWR)` on all open sockets. Blocking `read`/`recvfrom`/`accept` return errors immediately. Atomic flags are checked at loop boundaries. + +### `nxst::Result` (`include/nxst/domain/result.hpp`) + +85-line tagged-union result type. No exceptions, no `std::variant`. Used for `io::backup` and `io::restore` return values. + +```cpp +auto result = io::backup(title_index, uid); +if (!result.isOk()) { + failSend("Backup failed: " + result.error()); + return; +} +fs::path dir = result.value(); +``` + +`Result` specialisation available for operations that succeed without a value. + +### `nxst::FsFileSystemHandle` / `nxst::FileHandle` (`include/nxst/infra/fs/handles.hpp`) + +RAII wrappers ensuring `fsFsClose` / `fclose` are called on all exit paths. + +--- + +## Threading Invariants + +- All `std::atomic` members in `TransferService` use sequential-consistency (default). No explicit `memory_order` needed at this concurrency level. +- `TransferState::status` is a `std::string` protected by `std::mutex status_mutex`. All other `TransferState` fields are atomics. +- `pthread_create` + `pthread_detach` is used throughout (libnx's supported path). Threads are never joined — they signal completion via `state.done = true` and set their active flag to false. +- Cancel is safe to call from any thread. + +--- + +## File Map + +``` +include/nxst/ +├── app/ +│ ├── main.hpp — global state (g_currentTime, g_sortMode, …) +│ └── main_application.hpp — MainApplication : pu::ui::Application +├── domain/ +│ ├── account.hpp — AccountUid, Account::ids(), Account::username() +│ ├── common.hpp — StringUtils (UTF-8/16, elide, accents) +│ ├── protocol.hpp — wire constants (ports, sentinel, buffer size) +│ ├── result.hpp — Result tagged union +│ ├── title.hpp — Title, getTitle(), getTitleCount() +│ ├── transfer_state.hpp — TransferState (atomics + mutex-guarded status) +│ └── util.hpp — StringUtils helpers, blinkLed +├── infra/ +│ ├── fs/ +│ │ ├── directory.hpp — Directory iterator (libnx FsDir) +│ │ ├── filesystem.hpp — FileSystem::mount/unmount wrappers +│ │ ├── handles.hpp — FsFileSystemHandle, FileHandle (RAII) +│ │ └── io.hpp — io::backup, io::restore, io::copyFile, … +│ ├── net/ +│ │ └── socket.hpp — Socket RAII wrapper (int fd) +│ └── sys/ +│ └── logger.hpp — nxst::log::info/warn/error (printf-checked) +├── service/ +│ └── transfer_service.hpp — TransferService +└── ui/ + ├── card.hpp — Card UI component + ├── const.h — layout/color/font constants + ├── header_bar.hpp — HeaderBar (title + user avatar row) + ├── hint_bar.hpp — HintBar (button legend) + ├── theme.hpp — color, radius, spacing, type tokens + ├── titles_layout.hpp — TitlesLayout + ├── transfer_overlay.hpp — TransferOverlay (progress modal) + ├── ui_context.hpp — UiContext (selected user state) + └── users_layout.hpp — UsersLayout +``` + +--- + +## Build + +```bash +# Configure (once — toolchain preset reads $DEVKITPRO automatically) +cmake --preset switch + +# Build +cmake --build build + +# Send to Switch via nxlink +cmake --build build --target send +``` + +Output: `build/NXST.nro` + +See `README.md` for full build prerequisites. diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md new file mode 100644 index 0000000..896c863 --- /dev/null +++ b/docs/PROTOCOL.md @@ -0,0 +1,95 @@ +# NXST Wire Protocol + +## Overview + +NXST uses two sockets per transfer session: + +| Socket | Purpose | +|--------|---------| +| UDP multicast | Receiver advertisement (discovery) | +| TCP | File data stream | + +Both sides must be on the same local network segment. The sender (Transfer mode) initiates; the receiver (Receive mode) listens. + +--- + +## Discovery — UDP Multicast + +| Parameter | Value | +|-----------|-------| +| Group | `239.0.0.1` | +| Port | `8081` | +| Direction | sender → receiver | + +**Flow:** + +1. Receiver joins multicast group and binds `0.0.0.0:8081`. +2. Sender sends `"DISCOVER_SERVER"` (15 bytes, no null terminator) to `239.0.0.1:8081`. +3. Receiver replies `"SERVER_HERE"` (11 bytes) to the sender's source address. +4. Sender extracts the receiver's IP from the reply source and closes the UDP socket. + +Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice. + +--- + +## File Transfer — TCP + +| Parameter | Value | +|-----------|-------| +| Port | `8080` | +| Direction | sender connects → receiver listens | +| Buffer size | 65 536 bytes (`proto::BUF_SIZE`) | + +**Connection:** + +1. Receiver listens on `0.0.0.0:8080` (started concurrently with multicast listener). +2. Sender connects after receiving `"SERVER_HERE"`. + +**Wire layout — one file:** + +``` +┌─────────────────────────────────┐ +│ filename_len │ uint32_t LE │ 4 bytes +├─────────────────────────────────┤ +│ filename │ filename_len │ bytes, no null terminator +│ │ bytes │ +├─────────────────────────────────┤ +│ file_size │ uint64_t LE │ 8 bytes +├─────────────────────────────────┤ +│ file_data │ file_size │ bytes +│ │ bytes │ +└─────────────────────────────────┘ +``` + +Files are sent sequentially. The stream ends with a sentinel frame: + +``` +filename_len == 0 (proto::EOF_SENTINEL) +``` + +No `filename` or `file_size` field follows the sentinel. + +**Constraints:** + +- `filename_len > proto::MAX_FILENAME` (4 096) is treated as a protocol error; the receiver aborts. +- Filenames are full paths as produced by `io::backup` (e.g. `/switch/NXST//<user>/...`). +- On the receiver, the username path component is rewritten to match the local user's nickname before writing to disk. + +--- + +## Post-Transfer + +After the TCP stream closes (sentinel received), the receiver calls `io::restore`: + +1. Mounts the title's save filesystem. +2. Clears existing save data. +3. Copies received files into `save:/`. +4. Commits via `fsdevCommitDevice("save")`. + +The sender creates a local backup via `io::backup` before connecting, so the sender's own save is never at risk. + +--- + +## Cancellation + +Either side can cancel at any time by closing the relevant socket (`shutdown` + `close`). The other side's blocking read/write will return an error and the transfer loop exits cleanly. The receiver's accept thread sets `receiver_state.done = true` regardless of how the connection ends, so the UI poll loop always terminates.