refactoring
CI / Build NRO (push) Successful in 28s
CI / Format check (push) Successful in 33s
CI / Layering check (push) Successful in 1s

Co-authored-by: n.fedorov <mail@nfedorov.dev>
Co-committed-by: n.fedorov <mail@nfedorov.dev>
This commit was merged in pull request #1.
This commit is contained in:
2026-05-14 23:34:13 +03:00
committed by DragonSpirit
parent 844093e3e7
commit 1111f691c6
81 changed files with 3685 additions and 3036 deletions
+29
View File
@@ -0,0 +1,29 @@
---
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 110
PointerAlignment: Left
AlignAfterOpenBracket: Align
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: Empty
BreakBeforeBraces: Attach
SortIncludes: true
IncludeBlocks: Regroup
IncludeCategories:
# Project headers: nxst/
- Regex: '^(<|")(nxst/)'
Priority: 3
SortPriority: 3
# Third-party: Plutonium, libnx, SDL, switch.h
- Regex: '^(<|")(pu/|switch\.h|libnx|SDL|freetype|harfbuzz|zlib)'
Priority: 2
SortPriority: 2
# System / C++ standard library
- Regex: '^<'
Priority: 1
SortPriority: 1
SpacesBeforeTrailingComments: 2
Cpp11BracedListStyle: true
Standard: c++17
NamespaceIndentation: All
+47
View File
@@ -0,0 +1,47 @@
---
Checks: >
bugprone-*,
readability-*,
modernize-*,
cppcoreguidelines-pro-type-cstyle-cast,
-fuchsia-*,
-google-*,
-llvm-*,
-readability-magic-numbers,
-readability-named-parameter,
-modernize-use-trailing-return-type,
-modernize-use-nodiscard,
-modernize-avoid-c-arrays,
-bugprone-easily-swappable-parameters
WarningsAsErrors: ''
HeaderFilterRegex: 'include/nxst/.*'
CheckOptions:
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.StructCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: camelBack
- key: readability-identifier-naming.MethodCase
value: camelBack
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: lower_case
- key: readability-identifier-naming.MemberCase
value: lower_case
- key: readability-identifier-naming.NamespaceCase
value: lower_case
- key: readability-identifier-naming.ConstexprVariablePrefix
value: 'k'
- key: readability-identifier-naming.ConstexprVariableCase
value: CamelCase
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-braces-around-statements.ShortStatementLines
value: '2'
+22
View File
@@ -0,0 +1,22 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{cpp,hpp,h,c}]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.{json,md}]
indent_style = space
indent_size = 2
+10
View File
@@ -0,0 +1,10 @@
# Normalize line endings
* text=auto eol=lf
# Binary assets
*.png binary
*.jpg binary
*.nro binary
*.nso binary
*.pfs0 binary
*.elf binary
+79
View File
@@ -0,0 +1,79 @@
name: CI
on:
push:
pull_request:
jobs:
nro:
name: Build NRO
runs-on: ubuntu-latest
steps:
- name: Build
run: |
AUTH_URL=$(echo "$GITHUB_SERVER_URL" | sed "s|://|://oauth2:${GITHUB_TOKEN}@|")
docker rm -f nxst-build 2>/dev/null || true
docker run --name nxst-build devkitpro/devkita64:latest \
bash -c "
git clone --recurse-submodules '${AUTH_URL}/${GITHUB_REPOSITORY}' /src &&
cd /src && git checkout '${GITHUB_SHA}' &&
cmake --preset switch &&
cmake --build build -j\$(nproc)
"
docker cp nxst-build:/src/build/NXST.nro ./NXST.nro
docker rm nxst-build
- name: Publish release
if: startsWith(github.ref, 'refs/tags/v')
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
release_id=$(curl -sf -X POST "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${GITHUB_REF_NAME}\", \"name\": \"${GITHUB_REF_NAME}\"}" \
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
curl -sf -X POST "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/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:
- name: Checkout
run: |
AUTH_URL=$(echo "$GITHUB_SERVER_URL" | sed "s|://|://oauth2:${GITHUB_TOKEN}@|")
git clone "${AUTH_URL}/${GITHUB_REPOSITORY}" .
git checkout "$GITHUB_SHA"
- name: Install clang-format
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update && sudo apt-get install -y clang-format-22
sudo ln -sf /usr/bin/clang-format-22 /usr/local/bin/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:
- name: Checkout
run: |
AUTH_URL=$(echo "$GITHUB_SERVER_URL" | sed "s|://|://oauth2:${GITHUB_TOKEN}@|")
git clone "${AUTH_URL}/${GITHUB_REPOSITORY}" .
git checkout "$GITHUB_SHA"
- name: UI must not include net/sys headers
run: |
! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/
+76
View File
@@ -0,0 +1,76 @@
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: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update && sudo apt-get install -y clang-format-22
sudo ln -sf /usr/bin/clang-format-22 /usr/local/bin/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/
+19 -2
View File
@@ -1,4 +1,6 @@
.DS_Store .DS_Store
.serena
.idea/
# Prerequisites # Prerequisites
*.d *.d
@@ -32,5 +34,20 @@
*.exe *.exe
*.out *.out
*.app *.app
server
client # Switch build artifacts
*.nro
*.nso
*.pfs0
*.nacp
*.elf
*.lst
*.map
# CMake
build/
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
compile_commands.json
.cache/
-3
View File
@@ -1,3 +0,0 @@
[submodule "lib"]
path = lib
url = https://github.com/XorTroll/Plutonium.git
+34
View File
@@ -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<T>` tagged-union type replacing `std::tuple<bool, Result, std::string>`
- 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
+121
View File
@@ -0,0 +1,121 @@
cmake_minimum_required(VERSION 3.20)
project(NXST
LANGUAGES CXX
VERSION 0.1.0
)
# ── C++ standard and flags ────────────────────────────────────────────────────
# Arch/linker/libnx flags are already injected by the Switch.cmake toolchain.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS ON) # gnu++17
add_compile_options(
-fno-rtti
-fno-exceptions
-O2
-g
-D_GNU_SOURCE=1
)
# Export compilation database (enables clangd / clang-tidy on the host)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ── Sources ───────────────────────────────────────────────────────────────────
file(GLOB_RECURSE NXST_SOURCES
src/app/*.cpp
src/domain/*.cpp
src/infra/net/*.cpp
src/infra/fs/*.cpp
src/infra/sys/*.cpp
src/service/*.cpp
src/ui/*.cpp
)
add_executable(NXST ${NXST_SOURCES})
# ── Include paths ─────────────────────────────────────────────────────────────
target_include_directories(NXST PRIVATE include)
# ── pkg-config (uses aarch64-none-elf-pkg-config set by Switch.cmake) ─────────
find_package(PkgConfig REQUIRED)
set(NXST_PKG_MODULES
SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer
freetype2 harfbuzz minizip libpng libjpeg libwebp
glesv2 egl glapi zlib
)
pkg_check_modules(PORTLIBS REQUIRED IMPORTED_TARGET ${NXST_PKG_MODULES})
target_include_directories(NXST PRIVATE ${PORTLIBS_INCLUDE_DIRS})
# ── Plutonium ─────────────────────────────────────────────────────────────────
include(ExternalProject)
set(PLUTONIUM_SOURCE_DIR ${CMAKE_BINARY_DIR}/plutonium)
set(LIBPU_A ${PLUTONIUM_SOURCE_DIR}/Plutonium/lib/libpu.a)
ExternalProject_Add(plutonium_ep
GIT_REPOSITORY https://github.com/XorTroll/Plutonium.git
GIT_TAG b56564b70d038c59eef875f2c3cf436859c827f2
GIT_PROGRESS ON
SOURCE_DIR ${PLUTONIUM_SOURCE_DIR}
CONFIGURE_COMMAND ""
BUILD_COMMAND make -j -C ${PLUTONIUM_SOURCE_DIR}/Plutonium
INSTALL_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_BYPRODUCTS ${LIBPU_A}
)
add_library(plutonium STATIC IMPORTED GLOBAL)
add_dependencies(plutonium plutonium_ep)
set_target_properties(plutonium PROPERTIES
IMPORTED_LOCATION ${LIBPU_A}
)
target_include_directories(NXST PRIVATE ${PLUTONIUM_SOURCE_DIR}/Plutonium/include)
# ── Link libraries ────────────────────────────────────────────────────────────
# Order matters for static linking: put most dependent libs first.
# drm_nouveau, harfbuzz, freetype, z appended explicitly after pkg-config output
# to fix the freetype→harfbuzz static link order (see build notes from libnx update).
target_link_libraries(NXST PRIVATE
plutonium
PkgConfig::PORTLIBS
drm_nouveau
harfbuzz
freetype
z
)
# ── NACP + NRO ────────────────────────────────────────────────────────────────
set(NXST_NACP ${CMAKE_CURRENT_BINARY_DIR}/NXST.nacp)
nx_generate_nacp(
OUTPUT ${NXST_NACP}
NAME "NXST"
AUTHOR "DragonSpirit"
VERSION "04.26.2026"
)
nx_create_nro(NXST
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro
ICON ${CMAKE_SOURCE_DIR}/icon.png
NACP ${NXST_NACP}
)
# ── Convenience targets ────────────────────────────────────────────────────────
find_program(NXLINK_EXE nxlink HINTS ${DEVKITPRO}/tools/bin)
if(NXLINK_EXE)
add_custom_target(send
COMMAND ${NXLINK_EXE} ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro
DEPENDS NXST_nro
COMMENT "Sending NXST.nro via nxlink"
)
add_custom_target(debug
COMMAND ${NXLINK_EXE} -s ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro
DEPENDS NXST_nro
COMMENT "Sending NXST.nro with stdio bridge"
)
endif()
+17
View File
@@ -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"
}
]
}
+30
View File
@@ -0,0 +1,30 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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 <https://www.gnu.org/licenses/gpl-3.0.txt>.
-------------------------------------------------------------------------------
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
-204
View File
@@ -1,204 +0,0 @@
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITPRO)),)
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/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):
# - <Project name>.jpg
# - icon.jpg
# - <libnx folder>/default_icon.jpg
#---------------------------------------------------------------------------------
TARGET := NXST
BUILD := build
SOURCES := source lib/Plutonium/source
INCLUDES := include include/net 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
#---------------------------------------------------------------------------------------
+434
View File
@@ -0,0 +1,434 @@
# NXST Architectural Refactor Plan
## Current Phase Tracker
| Phase | Title | Status | Effort | Notes |
|-------|-------|--------|--------|-------|
| 0 | Tooling & ground rules | ✅ Done | S (~2h) | `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes` |
| 1 | Bug fixes & dead code | ✅ Done | S (~3h) | logger rewrite, `mkdir 0777`, RU comment, dead code |
| 2 | File renames + `#pragma once` | ✅ Done | S (~2h) | snake_case filenames, unify guards |
| 3 | Directory restructure | ✅ Done | M (~1d) | `src/` + `include/nxst/` layered tree |
| 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<T>` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory |
| 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:** — 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.
---
## Context
NXST is a Nintendo Switch homebrew save-transfer app (~3.2K LOC across 13 .cpp + 24 .hpp files, plus the vendored Plutonium UI submodule). The current codebase works but reads as a prototype:
- Flat `source/` + mostly flat `include/` with mixed `PascalCase` and `lowercase` filenames.
- Mixed identifier style (`recv_all` next to `isServerTransferDone`).
- 11 headers use `#pragma once`, 9 use legacy `#ifndef X_HPP` guards.
- **Severe layering violation**: UI layouts (`TitlesLayout::runTransfer`, `runReceive`) call into raw socket code and `io::restore` directly.
- Globals: 6 in `server.cpp`, 4 in `client.cpp`, 2 in `io.cpp`, plus `g_currentUId` everywhere.
- **Logger is silently broken** (`include/logger.hpp:54`): `printf(StringUtils::format(...).c_str(), args...)` already substitutes the format, then passes leftover variadic args to a string with no remaining `%` specifiers — every error log loses its arguments.
- **Permission bug** in `source/io.cpp:119`: `mkdir(path.c_str(), 777)` — decimal 777 is octal 1411 (no read for owner). Has been wrong since day one.
- One monster function: `io::restore` is 112 lines mixing mount, create, clear, copy, commit.
- Russian comment in `io.cpp:231` (mixed languages).
- Dead commented-out code in `util.cpp:50-58` and `logger.hpp:51-54`.
- No `.clang-format`, no `.clang-tidy`, no `.editorconfig`, no `.gitattributes`, no CI, no `README.md`, no `ARCHITECTURE.md`, no `LICENSE` file (despite `io.cpp` carrying GPLv3 from Checkpoint).
Goal: transform NXST into a layered, conventionally-named, tooled-up project that builds on Switch via CMake and is readable by a stranger in under ten minutes. Solo developer; bar is "professional indie", not enterprise.
User decisions (already made):
- **Full 8-phase plan**.
- **Migrate Make → CMake** with devkitpro toolchain file.
- **camelCase for functions, PascalCase for classes**, snake_case for files and locals, `kPascalCase` for constants.
- **No tests** (Switch homebrew rarely has them; pure-logic surface is small).
---
## Target Directory Tree
```
NXST/
├── .clang-format # LLVM-derived, 4-space, 110 col
├── .clang-tidy # bugprone-*, readability-*, modernize-*
├── .editorconfig # LF, UTF-8, trim WS, final newline
├── .gitattributes # eol=lf; lib/Plutonium linguist-vendored
├── .github/workflows/build.yml # CI: devkitpro container, cmake, .nro upload
├── docs/
│ ├── ARCHITECTURE.md
│ ├── PROTOCOL.md
│ └── screenshots/
├── cmake/
│ └── modules/ # FindPlutonium.cmake (if needed)
├── include/nxst/
│ ├── app/ # MainApplication, AppContext
│ ├── domain/ # Title, Account, TransferState, Result
│ ├── infra/
│ │ ├── fs/ # filesystem, directory, RAII handles
│ │ ├── net/ # socket, sendAll/recvAll
│ │ └── sys/ # logger, threading, switch services
│ ├── service/ # TransferService
│ └── ui/ # layouts, widgets, theme
├── src/ # mirrors include/nxst/
│ ├── app/
│ ├── domain/
│ ├── infra/{fs,net,sys}/
│ ├── service/
│ ├── ui/
│ └── main.cpp
├── tools/format.sh # one-line clang-format helper
├── lib/Plutonium/ # submodule, untouched
├── deps/asprintf/ # untouched
├── CMakeLists.txt # top-level
├── README.md
├── ARCHITECTURE.md # → docs/ARCHITECTURE.md
├── CHANGELOG.md
├── LICENSE # GPLv3 (forced by Checkpoint inheritance)
└── icon.png
```
---
## Naming Convention Spec
| Axis | Rule | Examples |
|---|---|---|
| Files | `snake_case.{cpp,hpp}` | `transfer_service.cpp`, `socket.hpp` |
| Classes / structs | `PascalCase` | `TransferService`, `AccountProfile`, `Socket` |
| Functions / methods | `camelCase` | `sendAll()`, `recvAll()`, `replaceUsername()` |
| Locals / params | `snake_case` | `int sock_fd`, `std::string file_name` |
| Members | `snake_case`, no prefix | `bytes_done`, `client_sock` |
| Constants / constexpr | `kPascalCase` | `kTcpPort`, `kBufSize`, `kMulticastGroup` |
| Enums | `enum class Foo { Bar, Baz }` | `enum class TransferKind { Send, Receive }` |
| Macros (avoid) | `NXST_UPPER_SNAKE` | `NXST_UNREACHABLE` |
| Namespaces | lowercase, short | `nxst`, `nxst::net`, `nxst::ui` |
---
## Layering (dependency direction)
```
┌────────────────────────┐
│ Presentation (ui/) │
│ layouts, widgets │
└───────────┬────────────┘
┌───────────▼────────────┐
│ Service (svc/) │
│ TransferService │
└───────────┬────────────┘
┌────────────────────┼────────────────────┐
│ │ │
┌──────▼──────┐ ┌────────▼──────┐ ┌────────▼──────┐
│ Domain │ │ Infra/net │ │ Infra/fs │ Infra/sys
│ Title, │ │ Socket, │ │ Directory, │ Logger,
│ Account, │ │ sendAll, │ │ FsHandle, │ Thread,
│ Result<T> │ │ recvAll │ │ FileHandle │ Account svc
└─────────────┘ └───────────────┘ └───────────────┘
libnx, SDL, Plutonium
```
**Hard 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 `<arpa/inet.h>`, `<sys/socket.h>`, `pthread.h`, or call `recv`/`send` directly**. `app/` wires everything.
CI enforcement: a 20-line shell step greps `src/ui/**` for forbidden headers.
---
## Phase 0 — Tooling and ground rules (S, ~2h)
**Goal.** Lay down conventions before touching code.
**Files created.**
- `.clang-format`: BasedOnStyle LLVM, IndentWidth 4, ColumnLimit 110, PointerAlignment Left, IncludeBlocks Regroup with three categories (system, third-party, project).
- `.clang-tidy`: enable `bugprone-*`, `readability-*` (minus `magic-numbers`), `modernize-*` (minus `use-trailing-return-type`, `use-nodiscard`), `cppcoreguidelines-pro-type-cstyle-cast`. Disable `fuchsia-*`, `google-*`, `llvm-header-guard`.
- `.editorconfig`: LF, UTF-8, 4-space, trim trailing whitespace, final newline. Tab indent for `Makefile` (only while it still exists).
- `.gitattributes`: `* text=auto eol=lf`, `lib/Plutonium/** linguist-vendored=true`, `deps/** linguist-vendored=true`. Stops GitHub mis-classifying the repo language.
- `tools/format.sh`: `clang-format -i` over `src/` + `include/`.
**Risk.** None — no compiled changes.
**Buildable?** Unaffected.
---
## Phase 1 — Bug fixes and dead code (S, ~3h)
**Goal.** Fix concrete bugs before any structural change can mask them.
**Tasks.**
1. Replace broken `Logger` (`include/logger.hpp`) with free-function API in `include/nxst/infra/sys/log.hpp`:
```cpp
namespace nxst::log {
enum class Level { Debug, Info, Warn, Error };
void write(Level, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
void info (const char* fmt, ...) __attribute__((format(printf, 1, 2)));
void warn (const char* fmt, ...) __attribute__((format(printf, 1, 2)));
void error(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
}
```
Implementation: `vsnprintf` into stack buffer, prefix with `[YYYY-MM-DD HH:MM:SS][LEVEL]`, append to `/switch/NXST/log.log` and stderr. TU-local `std::mutex`. No singleton class. Drop the GPLv3 Checkpoint header (the new file is original).
- `format(printf, ...)` attribute gives compile-time format-string checking — that is the actual reason this rewrite matters.
- Leave thin shim `LOG_INFO(...)`/`LOG_ERROR(...)` macros so existing call sites keep compiling during phase migration.
2. Fix `source/io.cpp:119` — `mkdir(path.c_str(), 777)` → `mkdir(path.c_str(), 0777)`.
3. Translate Russian comment at `source/io.cpp:231` to English.
4. Delete dead commented-out code in `source/util.cpp:50-58` and old `include/logger.hpp:51-54` block.
**Risk.** Logger API change ripples to <30 call sites. Macros bridge the gap.
**Buildable?** Yes after each file.
---
## Phase 2 — File renames + `#pragma once` (S, ~2h)
**Goal.** Land the filename convention before the directory move so git history sees rename + move as separate commits.
**Tasks.**
- Rename to `snake_case` via `git mv` (use two-step on case-insensitive macOS): `Main.cpp`→`main.cpp`, `MainApplication.{cpp,hpp}`→`main_application.{cpp,hpp}`, `TitlesLayout.{cpp,hpp}`→`titles_layout.{cpp,hpp}`, `UsersLayout.{cpp,hpp}`→`users_layout.{cpp,hpp}`, plus matching headers under `include/`.
- Convert all `#ifndef X_HPP / #define X_HPP / #endif` guards to `#pragma once`. Affected: `logger.hpp`, `account.hpp`, and the 7 other headers using legacy guards.
- Update affected `#include` lines.
- `Makefile` `SOURCES` globs `*.cpp`, so no Makefile change needed for renames.
**Risk.** macOS case-insensitive FS hides case-only renames — use `git mv Foo.cpp tmp.cpp && git mv tmp.cpp foo.cpp`.
**Buildable?** Yes after each pair.
---
## Phase 3 — Directory restructure (M, ~1 day)
**Goal.** Move flat `source/` and `include/` into the layered tree. **No code changes**, just relocation + include-path updates.
**Tasks.**
1. Create the `src/{app,domain,infra/{net,fs,sys},service,ui}` and `include/nxst/{...}` skeleton.
2. Move files:
- `protocol.hpp`, `transfer_state.hpp`, `account.hpp` (the `AccountUid` struct + ordering only), `title.hpp/cpp`, `common.hpp/cpp`, `util.hpp/cpp` → `domain/`.
- `client.{hpp,cpp}` → `infra/net/transfer_sender.{hpp,cpp}`, `server.{hpp,cpp}` → `infra/net/transfer_receiver.{hpp,cpp}`. Renaming is intentional: `CLAUDE.md` already documents that "server = receiver, client = sender" is inverted from typical usage; renaming kills the confusion permanently.
- `net/socket.hpp` → `infra/net/socket.hpp`.
- `io.hpp/cpp`, `directory.hpp/cpp`, `filesystem.hpp/cpp` → `infra/fs/`.
- new `log.hpp/cpp`, switch-service init pieces from `account.cpp` → `infra/sys/`.
- `main_application.{hpp,cpp}`, `main.{hpp,cpp}` → `app/`.
- `titles_layout.*`, `users_layout.*`, `transfer_overlay.hpp`, `theme.hpp`, `ui/*.hpp`, `header_bar.hpp` → `ui/`.
3. Update `Makefile` (still in use — CMake migration is Phase 4):
```make
SOURCES := src 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
```
Remove the now-defunct `include/net` from `INCLUDES`.
4. Update **every** `#include` to `#include <nxst/...>` form: `#include <nxst/domain/protocol.hpp>`, `#include <nxst/infra/net/socket.hpp>`. The single root `include/` keeps the prefix mandatory and grepable.
**Risk.** Big disruptive phase. Do on a feature branch, build after every directory's worth of moves. `service/` directory stays empty for now — scaffolding only.
**Buildable?** Yes if you move-and-rebuild incrementally.
---
## Phase 4 — Migrate Make → CMake (M, ~1 day)
**Goal.** Replace `Makefile` with `CMakeLists.txt` using devkitpro's bundled `Switch.cmake` toolchain file. Project compiles via `cmake -B build -DCMAKE_TOOLCHAIN_FILE=$DEVKITPRO/cmake/Switch.cmake && cmake --build build`.
**Tasks.**
1. Create top-level `CMakeLists.txt`:
- `cmake_minimum_required(VERSION 3.20)`, `project(NXST CXX)`.
- C++17, `-fno-rtti -fno-exceptions` (matches current `CXXFLAGS`).
- ARM flags `-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE` mirror current `ARCH`.
- `find_package(PkgConfig REQUIRED)` + `pkg_check_modules(SWITCH_LIBS REQUIRED SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer freetype2 harfbuzz minizip libpng libjpeg libwebp glesv2 egl glapi zlib)` against `aarch64-none-elf-pkg-config` (already wired up per memory ID 101).
- Manual link: `pu` (Plutonium), `drm_nouveau`, plus the trailing `-lharfbuzz -lfreetype -lz` static-link-order workaround (memory IDs 97, 98, 100 — required after recent libnx update).
- Add Plutonium as `add_subdirectory(lib/Plutonium)` if its CMake exists, else as an INTERFACE library wrapping its prebuilt `.a`. Inspection during this phase decides.
- Use devkitpro's `nx_create_nro` / `nx_generate_nacp` helpers from the toolchain file to emit `NXST.nro` with icon + NACP metadata (`APP_TITLE=NXST`, `APP_AUTHOR=DragonSpirit`, `APP_VERSION=04.26.2026`).
2. Create `cmake/modules/` if any custom Find module proves necessary (likely not).
3. Add convenience targets: `cmake --build build --target send` to invoke `nxlink NXST.nro`.
4. Delete old `Makefile` only after CMake build produces a working `.nro` matching the Make output.
5. Update `.gitignore`: replace `build/` Make artifacts with CMake's `build/` directory rules and `CMakeCache.txt`, `CMakeFiles/`, `compile_commands.json`.
**Risk.** Highest in the plan. Mitigation:
- Keep `Makefile` alongside CMake during the phase. Verify `.nro` from both is identical (or at minimum boots and runs on hardware).
- If the devkitpro `Switch.cmake` toolchain proves brittle for Plutonium's transitive headers, fall back to keeping Make and document why CMake was deferred.
- The pkg-config tweaks captured in memory IDs 96101 are non-obvious — re-apply them in the CMake config explicitly, not via `pkg_check_modules` defaults.
**Buildable?** Yes — both build systems coexist until CMake is verified.
---
## Phase 5 — TransferService extraction, kill globals (L, ~2 days)
**Goal.** Sever the UI → net coupling. Single biggest "this looks like real software" change.
**Tasks.**
1. Create `src/service/transfer_service.{hpp,cpp}`. Class owns:
- one `TransferState` per direction (sender, receiver),
- all listen / accept / broadcast threads (currently 6 globals in `server.cpp`, 4 in `client.cpp`),
- public API: `start(TransferKind, AccountUid, std::vector<TitleId>)`, `cancel()`, `progress()`, `statusText()`, `isDone()`, `failureReason()`, `setOnComplete(std::function<void(TransferResult)>)`.
2. Move file globals (`g_server_state`, `g_client_state`, all socket/pthread atomics) into `TransferService` private members. pthread C trampolines: `static void* threadEntry(void* self)` immediately calls `*static_cast<TransferService*>(self)`.
3. Refactor `titles_layout.cpp` `runTransfer`/`runReceive` to call `app_ctx.transfer.start(...)` / `cancel()` instead of free `transfer_files()` / `start_listening()`. Inject via `AppContext&` (already partially present per `include/ui/UiContext.hpp`).
4. Move `g_currentUId` out of global scope into `AppContext::current_user`. Pass `AppContext&` down to layouts at construction. There is exactly one `AppContext` and its lifetime equals the app — safe and cheap.
5. UI's `runReceive` currently calls `io::restore` directly after server work completes. Move into the service's completion callback so UI never touches FS directly.
**Strategy.** Keep old `transfer_files()` / `start_listening()` free functions as thin wrappers around `TransferService` for the duration of the phase. Convert UI call sites one at a time. Delete wrappers only after both sites are on the service.
**Buildable?** Yes via the wrappers.
---
## Phase 6 — `Result<T>` and RAII (M, ~1 day)
**Goal.** Replace bool/out-param/silent-failure patterns with `Result<T>`; wrap raw OS handles in RAII.
**Tasks.**
1. `include/nxst/domain/result.hpp` — minimal `Result<T, E=std::string>` with tagged union (no `std::variant` because `-fno-exceptions` rules out monostate-on-throw):
```cpp
template <class T, class E = std::string>
class Result {
bool ok_;
union { T val_; E err_; };
public:
static Result success(T v);
static Result failure(E e);
bool isOk() const noexcept;
const T& value() const; // UB if !isOk
const E& error() const; // UB if isOk
template <class F> auto map(F&&) const;
};
```
Resist importing `tl::expected`. 60 lines of in-house code beats a vendored dep at this scale.
2. RAII wrappers in `include/nxst/infra/`:
- `FdHandle` — owns `int fd`, closes on dtor, move-only.
- `FsFileSystemHandle` — owns `FsFileSystem`, calls `fsFsClose`. Plugs the "save"-mount leak from memory ID 59.
- `AccountProfileHandle` — owns `AccountProfile`, calls `accountProfileClose`. Plugs the bug from memory ID 64.
- `FileHandle` — owns `FILE*`, `fclose` on dtor.
3. Replace `io.cpp:57` `new u8[BUFFER_SIZE] / delete[]` and `io.cpp:234` `malloc/free` with `std::vector<u8>` (already the pattern in `client.cpp`/`server.cpp`).
4. Split `io::restore` (the 112-line monster, `io.cpp:221-332`) into `restoreSaveForTitle`, `extractAndCommit`, `clearStaleSaveFiles`. Each returns `Result<void>`.
5. Convert `recvAll` / `sendAll` / socket setup paths in `infra/net` to return `Result<void>` so error paths compose under `-fno-exceptions`.
**Strategy.** Adopt incrementally — start at IO boundaries, leave `bool` returns untouched in modules not yet refactored.
**Buildable?** Yes per file.
---
## Phase 7 — Documentation + license (S, ~half day)
**Goal.** A stranger can build, run, and understand this in under 15 minutes.
**Files created.**
- `README.md`: what NXST does, screenshot, install (drop `.nro` into `/switch/`), build (devkitpro + `cmake --build`), credit Plutonium and Checkpoint, link to `docs/`.
- `docs/ARCHITECTURE.md`: layering diagram from this plan, threading model (one accept thread, one broadcast thread, one transfer thread per direction; cancellation via `shutdown(2)` + atomic flag), pointers to key files.
- `docs/PROTOCOL.md`: promote the 9-line wire-format comment from `protocol.hpp` into a real spec — byte-level diagrams, multicast discovery on `239.0.0.1:8081`, TCP transfer on `:8080`, EOF sentinel = `filename_len == 0`, `kBufSize=65536`, `kMaxFilename=4096`.
- `CHANGELOG.md`: Keep-a-Changelog format. Version `0.x` until protocol stabilizes.
- `LICENSE`: GPLv3 — forced by `io.cpp` carrying Checkpoint's GPLv3 header. Document credit + license inheritance in README.
**Risk.** None.
**Buildable?** Unaffected.
---
## Phase 8 — CI (S, ~2h)
**Goal.** Every push verifies `.nro` builds. One green badge tells visitors the repo is alive.
**Tasks.**
- `.github/workflows/build.yml`:
```yaml
on: [push, pull_request]
jobs:
nro:
runs-on: ubuntu-latest
container: devkitpro/devkita64:latest
steps:
- uses: actions/checkout@v4
with: { submodules: recursive }
- run: |
cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=$DEVKITPRO/cmake/Switch.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
- uses: actions/upload-artifact@v4
with:
name: NXST-${{ github.sha }}
path: build/NXST.nro
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
sudo apt-get install -y clang-format
find src include -name '*.cpp' -o -name '*.hpp' \
| xargs clang-format --dry-run --Werror
layering:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
! grep -rE '^#include\s*[<"](arpa/|sys/socket|pthread\.h)' src/ui/
```
- Optional release job on tag `v*`: same CMake build, attach `.nro` to GitHub Release.
**Risk.** First run typically fails on submodule init or container path — iterate on a branch.
**Buildable?** Pass or fix.
---
## Critical files to modify
- `Makefile` — Phase 3 SOURCES/INCLUDES update; deleted in Phase 4 after CMake parity.
- `include/logger.hpp` — Phase 1 replace with `nxst::log` free-function API.
- `source/io.cpp` — Phase 1 mkdir + comment fix; Phase 6 split `restore()` + RAII; Phase 5 stop being called from UI.
- `source/server.cpp` — Phase 3 rename to `transfer_receiver.cpp`; Phase 5 globals migrate into `TransferService`.
- `source/client.cpp` — Phase 3 rename to `transfer_sender.cpp`; Phase 5 globals migrate into `TransferService`.
- `source/TitlesLayout.cpp` — Phase 2 rename; Phase 5 sever direct calls into network and `io::restore`.
- `source/Main.cpp` — Phase 2 rename; Phase 5 construct `AppContext` with `TransferService`.
- `source/util.cpp` — Phase 1 delete dead block.
New files (not yet present):
- `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes` — Phase 0.
- `CMakeLists.txt` — Phase 4.
- `include/nxst/domain/result.hpp` — Phase 6.
- `include/nxst/service/transfer_service.hpp` + `src/service/transfer_service.cpp` — Phase 5.
- `README.md`, `docs/ARCHITECTURE.md`, `docs/PROTOCOL.md`, `CHANGELOG.md`, `LICENSE` — Phase 7.
- `.github/workflows/build.yml` — Phase 8.
---
## What is deliberately NOT in this plan
1. **C++20.** Devkitpro's `gnu++17` is fine; nothing in the codebase wants concepts/ranges/coroutines, and coroutines under `-fno-exceptions` are a maze. Stay on `gnu++17`.
2. **Abstracting Plutonium.** It is *the* UI framework; you will not port it. UI calls Plutonium directly.
3. **`tl::expected` / `std::expected`.** 60-line in-house `Result<T>` is enough.
4. **DI containers, factories, abstract logger interfaces.** Solo developer, one service, one logger sink. Add complexity only when a second sink actually appears.
5. **Integration tests against the network.** No bug class is meaningfully prevented; cost is high; value comes from running on real hardware.
6. **Migrating from pthread to `std::thread`.** Libnx's pthread is the supported path. A single `Thread` RAII wrapper inside `infra/sys/` is the most you should add.
7. **PImpl broadly.** At 2.3 KLOC compile time is sub-second; PImpl just adds indirection.
8. **Re-vendoring or modifying Plutonium.** Submodule, untouched. `linguist-vendored` keeps it out of GitHub language stats.
9. **UPPER_SNAKE constants.** Visually clash with macros; `kPascalCase` is the modern norm.
---
## Recommended phasing for a solo developer
- **Weekend 1:** Phase 0 + 1 + 2. Tooled, bugs fixed, files renamed, builds. Already feels different.
- **Weekend 2:** Phase 3. Big move, one sitting on a branch.
- **Weekend 3:** Phase 4 (CMake). Build system swap. Highest individual phase risk — schedule when fresh.
- **Weekend 4 (longest):** Phase 5 (TransferService). UI no longer touches sockets. The win.
- **Weekend 5:** Phase 6. `Result<T>`, RAII, `restore()` split.
- **Evening:** Phase 7. README + ARCHITECTURE + LICENSE.
- **Evening:** Phase 8. CI.
Stopping after Weekend 4 still leaves the project visibly more serious than it started. Each weekend produces a defensible "ship it" state.
---
## Verification (end-to-end test plan)
After each phase:
1. `cmake --build build` (Phase 4+) or `make` produces `NXST.nro` with no warnings.
2. Copy `NXST.nro` to Switch SD card `/switch/NXST/`, launch via hbmenu, verify the title list and user list render (regression tests for memory IDs 73, 78).
3. Run a save transfer between two Switches: select a title on the sender, "Receive" on the receiver, verify the file lands and `io::restore` replaces it on the receiver. Verify cancel works mid-transfer (regression for memory IDs 60, 103, 104).
4. Verify the log file at `/switch/NXST/log.log` contains formatted entries with timestamps and levels (regression for the broken-Logger bug).
5. Phase 8 only: confirm a green CI run on a PR; download the `NXST.nro` artifact; install it on Switch; run the same flow.
+88
View File
@@ -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 (Plutonium UI fetched automatically by CMake)
git clone 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<T>, 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.
-37
View File
@@ -1,37 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include "asprintf.h"
int vasprintf(char **strp, const char *fmt, va_list ap) {
int size, res;
va_list cp;
va_copy(cp, ap);
size = vsnprintf(NULL, 0, fmt, cp);
va_end(cp);
if (size < 0)
return -1;
*strp = (char *)malloc(size + 1);
if (*strp == NULL)
return -1;
res = vsnprintf(*strp, size + 1, fmt, ap);
if (res < 0) {
free(*strp);
return -1;
}
return res;
}
int asprintf(char **s, const char *fmt, ...) {
int ret;
va_list ap;
va_start(ap, fmt);
ret = vasprintf(s, fmt, ap);
va_end(ap);
return ret;
}
-10
View File
@@ -1,10 +0,0 @@
#ifndef HAVE_ASPRINTF
#define HAVE_ASPRINTF 1
#include <stdarg.h>
int vasprintf(char **strp, const char *fmt, va_list ap);
int asprintf(char **s, const char *fmt, ...);
#endif
-9
View File
@@ -1,9 +0,0 @@
{
"name": "asprintf",
"version": "1.0.0",
"repo": "Neved4/asprintf",
"description": "asprintf, vasprintf - print to allocated string",
"license": "MIT",
"keywords": ["asprintf", "sprintf", "alloc", "string"],
"src": ["asprintf.c", "asprintf.h"]
}
+163
View File
@@ -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<T>, 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 `<arpa/inet.h>`, `<sys/socket.h>`, `<pthread.h>`, 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<T, E>` (`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<void, E>` 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<T, E> 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.
+95
View File
@@ -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::kBufSize`) |
**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::kEofSentinel)
```
No `filename` or `file_size` field follows the sentinel.
**Constraints:**
- `filename_len > proto::kMaxFilename` (4 096) is treated as a protocol error; the receiver aborts.
- Filenames are full paths as produced by `io::backup` (e.g. `/switch/NXST/<title>/<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.
-21
View File
@@ -1,21 +0,0 @@
#pragma once
#include <pu/Plutonium>
#include <UsersLayout.hpp>
#include <TitlesLayout.hpp>
namespace ui {
class MainApplication : public pu::ui::Application {
public:
using Application::Application;
PU_SMART_CTOR(MainApplication)
void OnLoad() override;
// Layout instance
UsersLayout::Ref usersLayout;
TitlesLayout::Ref titlesLayout;
};
}
-76
View File
@@ -1,76 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef ACCOUNT_HPP
#define ACCOUNT_HPP
#include <map>
#include <string.h>
#include <string>
#include <switch.h>
#include <vector>
#define USER_ICON_SIZE 64
namespace std {
template <>
struct hash<AccountUid> {
size_t operator()(const AccountUid& a) const { return ((hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1); }
};
}
inline bool operator==(const AccountUid& x, const AccountUid& y)
{
return x.uid[0] == y.uid[0] && x.uid[1] == y.uid[1];
}
inline bool operator==(const AccountUid& x, u64 y)
{
return x.uid[0] == y && x.uid[1] == y;
}
inline bool operator<(const AccountUid& x, const AccountUid& y)
{
if (x.uid[0] != y.uid[0]) return x.uid[0] < y.uid[0];
return x.uid[1] < y.uid[1];
}
struct User {
AccountUid id;
std::string name;
};
namespace Account {
Result init(void);
void exit(void);
std::vector<AccountUid> ids(void);
AccountUid selectAccount(void);
std::string username(AccountUid id);
std::string iconPath(AccountUid id);
}
#endif
-13
View File
@@ -1,13 +0,0 @@
#include <string>
#include <switch.h>
int transfer_files(size_t index, AccountUid uid);
bool isClientTransferDone();
bool isClientTransferCancelled();
bool isClientConnectionFailed();
bool isClientProgressKnown();
bool isClientWorkersIdle();
void cancelClientTransfer();
double getClientProgress();
std::string getClientStatusText();
std::string getClientFailReason();
-65
View File
@@ -1,65 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef COMMON_HPP
#define COMMON_HPP
#include <algorithm>
#include <arpa/inet.h>
#include <codecvt>
#include <cstdio>
#include <locale>
#include <memory>
#include <netinet/in.h>
#include <stdarg.h>
#include <string.h>
#include <string>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define ATEXIT(func) atexit((void (*)())func)
namespace DateTime {
std::string timeStr(void);
std::string dateTimeStr(void);
std::string logDateTime(void);
}
namespace StringUtils {
bool containsInvalidChar(const std::string& str);
std::string escapeJson(const std::string& s);
std::string format(const std::string fmt_str, ...);
std::string removeForbiddenCharacters(std::string src);
std::string UTF16toUTF8(const std::u16string& src);
void ltrim(std::string& s);
void rtrim(std::string& s);
void trim(std::string& s);
}
char* getConsoleIP(void);
#endif
-6
View File
@@ -1,6 +0,0 @@
#pragma once
#include <Theme.hpp>
#define COLOR(hex) pu::ui::Color::FromHex(hex)
#define BACKGROUND_COLOR theme::color::BgBase
-58
View File
@@ -1,58 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef DIRECTORY_HPP
#define DIRECTORY_HPP
#include <dirent.h>
#include <errno.h>
#include <string>
#include <switch.h>
#include <vector>
struct DirectoryEntry {
std::string name;
bool directory;
};
class Directory {
public:
Directory(const std::string& root);
~Directory() = default;
Result error(void);
std::string entry(size_t index);
bool folder(size_t index);
bool good(void);
size_t size(void);
private:
std::vector<struct DirectoryEntry> mList;
Result mError;
bool mGood;
};
#endif
-39
View File
@@ -1,39 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef FILESYSTEM_HPP
#define FILESYSTEM_HPP
#include "account.hpp"
#include <switch.h>
namespace FileSystem {
Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID);
int mount(FsFileSystem fs);
void unmount(void);
}
#endif
-55
View File
@@ -1,55 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef IO_HPP
#define IO_HPP
#include "account.hpp"
#include "directory.hpp"
#include "title.hpp"
#include "util.hpp"
#include <dirent.h>
#include <switch.h>
#include <sys/stat.h>
#include <tuple>
#include <unistd.h>
#include <utility>
#define BUFFER_SIZE 0x80000
namespace io {
std::tuple<bool, Result, std::string> backup(size_t index, AccountUid uid);
std::tuple<bool, Result, std::string> restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell);
Result copyDirectory(const std::string& srcPath, const std::string& dstPath);
void copyFile(const std::string& srcPath, const std::string& dstPath);
Result createDirectory(const std::string& path);
Result deleteFolderRecursively(const std::string& path);
bool directoryExists(const std::string& path);
bool fileExists(const std::string& path);
}
#endif
-85
View File
@@ -1,85 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef LOGGER_HPP
#define LOGGER_HPP
#include "common.hpp"
#include <stdio.h>
#include <string>
class Logger {
public:
static Logger& getInstance(void)
{
static Logger mLogger;
return mLogger;
}
inline static const std::string INFO = "[INFO]";
inline static const std::string DEBUG = "[DEBUG]";
inline static const std::string ERROR = "[ERROR]";
inline static const std::string WARN = "[WARN]";
template <typename... Args>
void log(const std::string& level, const std::string& format = {}, Args... args)
{
// buffer += StringUtils::format(("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
// buffer += StringUtils::format("%s\n", format.c_str());
// buffer += StringUtils::format("%s\n",StringUtils::format("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
printf(StringUtils::format("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
}
void flush(void)
{
mFile = fopen(mPath.c_str(), "a");
if (mFile != NULL) {
fprintf(mFile, buffer.c_str());
fprintf(stderr, buffer.c_str());
fclose(mFile);
}
}
private:
Logger(void) { buffer = ""; }
~Logger(void) {}
Logger(Logger const&) = delete;
void operator=(Logger const&) = delete;
#if defined(__SWITCH__)
const std::string mPath = "/switch/NXST/log.log";
#else
const std::string mPath = "log.log";
#endif
FILE* mFile;
std::string buffer;
};
#endif
-26
View File
@@ -1,26 +0,0 @@
#ifndef MAIN_HPP
#define MAIN_HPP
#include <const.h>
#include "account.hpp"
#include "title.hpp"
#include "util.hpp"
#include <memory>
#include <switch.h>
#include "logger.hpp"
typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t;
inline float g_currentTime = 0;
inline AccountUid g_currentUId;
inline bool g_backupScrollEnabled = 0;
inline bool g_notificationLedAvailable = false;
inline bool g_shouldExitNetworkLoop = false;
inline std::string g_selectedCheatKey;
inline std::vector<std::string> g_selectedCheatCodes;
inline u32 g_username_dotsize;
inline sort_t g_sortMode = SORT_ALPHA;
inline std::string g_currentFile = "";
inline bool g_isTransferringFile = false;
inline const std::string g_emptySave = "New...";
#endif
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include <unistd.h>
struct Socket {
int fd = -1;
Socket() = default;
explicit Socket(int fd) : fd(fd) {}
~Socket() { if (fd >= 0) close(fd); }
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
Socket(Socket&& o) : fd(o.fd) { o.fd = -1; }
operator int() const { return fd; }
bool valid() const { return fd >= 0; }
void release() { fd = -1; }
};
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <pu/Plutonium>
#include <nxst/service/transfer_service.hpp>
#include <nxst/ui/titles_layout.hpp>
#include <nxst/ui/users_layout.hpp>
namespace ui {
class MainApplication : public pu::ui::Application {
public:
using Application::Application;
PU_SMART_CTOR(MainApplication)
void OnLoad() override;
UsersLayout::Ref users_layout;
TitlesLayout::Ref titles_layout;
nxst::TransferService transfer;
};
} // namespace ui
+36
View File
@@ -0,0 +1,36 @@
// Copyright (C) 2024-2026 NXST contributors
#pragma once
#include <string>
#include <vector>
#include <switch.h>
// Hash and comparison support for AccountUid as map/unordered_map key.
namespace std {
template <> struct hash<AccountUid> {
size_t operator()(const AccountUid& a) const noexcept {
return (hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1;
}
};
} // namespace std
inline bool operator==(const AccountUid& x, const AccountUid& y) {
return x.uid[0] == y.uid[0] && x.uid[1] == y.uid[1];
}
inline bool operator<(const AccountUid& x, const AccountUid& y) {
return x.uid[0] != y.uid[0] ? x.uid[0] < y.uid[0] : x.uid[1] < y.uid[1];
}
struct User {
AccountUid id;
std::string name;
};
namespace account {
Result init();
void exit();
std::vector<AccountUid> ids();
std::string username(AccountUid id);
std::string iconPath(AccountUid id);
} // namespace account
+16
View File
@@ -0,0 +1,16 @@
#pragma once
namespace proto {
constexpr uint16_t kTcpPort = 8080;
constexpr uint16_t kMulticastPort = 8081;
constexpr char kMulticastGroup[] = "239.0.0.1";
constexpr size_t kBufSize = 65536;
constexpr uint32_t kMaxFilename = 4096;
constexpr uint32_t kEofSentinel = 0;
// Wire layout per file:
// [filename_len : uint32_t LE] — 0 == end-of-stream
// [filename : filename_len bytes]
// [file_size : uint64_t LE]
// [file_data : file_size bytes]
} // namespace proto
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#include <string>
#include <utility>
namespace nxst {
template <class T, class E = std::string> class Result {
bool ok;
alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)];
Result() = default;
public:
static Result success(T val) {
Result res;
res.ok = true;
new (res.storage) T(std::move(val));
return res;
}
static Result failure(E err) {
Result res;
res.ok = false;
new (res.storage) E(std::move(err));
return res;
}
~Result() {
if (ok)
reinterpret_cast<T*>(storage)->~T();
else
reinterpret_cast<E*>(storage)->~E();
}
Result(const Result& other) : ok(other.ok) {
if (ok)
new (storage) T(*reinterpret_cast<const T*>(other.storage));
else
new (storage) E(*reinterpret_cast<const E*>(other.storage));
}
Result(Result&& other) : ok(other.ok) {
if (ok)
new (storage) T(std::move(*reinterpret_cast<T*>(other.storage)));
else
new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
}
Result& operator=(const Result&) = delete;
bool isOk() const noexcept {
return ok;
}
const T& value() const {
return *reinterpret_cast<const T*>(storage);
}
const E& error() const {
return *reinterpret_cast<const E*>(storage);
}
};
// Specialisation for Result<void>
template <class E> class Result<void, E> {
bool ok;
alignas(E) unsigned char storage[sizeof(E)];
Result() = default;
public:
static Result success() {
Result res;
res.ok = true;
return res;
}
static Result failure(E err) {
Result res;
res.ok = false;
new (res.storage) E(std::move(err));
return res;
}
~Result() {
if (!ok)
reinterpret_cast<E*>(storage)->~E();
}
Result(const Result& other) : ok(other.ok) {
if (!ok)
new (storage) E(*reinterpret_cast<const E*>(other.storage));
}
Result(Result&& other) : ok(other.ok) {
if (!ok)
new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
}
Result& operator=(const Result&) = delete;
bool isOk() const noexcept {
return ok;
}
const E& error() const {
return *reinterpret_cast<const E*>(storage);
}
};
} // namespace nxst
+58
View File
@@ -0,0 +1,58 @@
// Copyright (C) 2024-2026 NXST contributors
#pragma once
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <switch.h>
class Title {
public:
void init(u8 save_data_type, u64 title_id, AccountUid uid, const std::string& name,
const std::string& author);
std::string author() const;
std::pair<std::string, std::string> displayName() const;
u64 id() const;
std::string name() const;
std::string path() const;
u64 playTimeNanoseconds() const;
std::string playTime() const;
void playTimeNanoseconds(u64 ns);
u32 lastPlayedTimestamp() const;
void lastPlayedTimestamp(u32 ts);
std::string fullPath(size_t index) const;
void refreshDirectories();
u64 saveId() const;
void saveId(u64 id);
std::vector<std::string> saves() const;
u8 saveDataType() const;
AccountUid userId() const;
std::string userName() const;
private:
u64 m_id{0};
u64 m_save_id{0};
AccountUid m_uid{};
std::string m_user_name;
std::string m_name;
std::string m_safe_name;
std::string m_author;
std::string m_path;
std::vector<std::string> m_saves;
std::vector<std::string> m_full_save_paths;
u8 m_save_data_type{0};
std::pair<std::string, std::string> m_display_name;
u64 m_play_time_ns{0};
u32 m_last_played_ts{0};
};
bool areTitlesLoaded();
void loadTitles();
void sortTitles();
void rotateSortMode();
void getTitle(Title& dst, AccountUid uid, size_t i);
size_t getTitleCount(AccountUid uid);
void refreshDirectories(u64 id);
std::unordered_map<std::string, std::string> getCompleteTitleList();
+22
View File
@@ -0,0 +1,22 @@
// Copyright (C) 2024-2026 NXST contributors
#pragma once
#include <switch.h>
void servicesExit();
Result servicesInit();
void blinkLed(u8 times);
namespace string_utils {
bool containsInvalidChar(const std::string& str);
std::string format(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
std::string removeForbiddenCharacters(std::string src);
std::string UTF16toUTF8(const std::u16string& src);
void ltrim(std::string& s);
void rtrim(std::string& s);
void trim(std::string& s);
std::string removeAccents(std::string str);
std::string removeNotAscii(std::string str);
std::u16string UTF8toUTF16(const char* src);
std::string elide(const std::string& s, size_t max_chars);
} // namespace string_utils
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2024-2026 NXST contributors
#pragma once
#include <string>
#include <vector>
#include <switch.h>
class Directory {
public:
explicit Directory(const std::string& path);
bool good() const {
return m_good;
}
Result error() const {
return m_error;
}
size_t size() const {
return m_entries.size();
}
std::string entry(size_t i) const;
bool folder(size_t i) const;
private:
struct Entry {
std::string name;
bool is_dir;
};
std::vector<Entry> m_entries;
Result m_error{0};
bool m_good{false};
};
+9
View File
@@ -0,0 +1,9 @@
// Copyright (C) 2024-2026 NXST contributors
#pragma once
#include <switch.h>
namespace file_system {
Result mount(FsFileSystem* fs, u64 title_id, AccountUid uid);
int mount(FsFileSystem fs);
void unmount();
} // namespace file_system
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include <cstdio>
#include <switch.h>
namespace nxst {
// RAII wrapper for FsFileSystem — auto-closes on destruction.
struct FsFileSystemHandle {
FsFileSystem fs{};
bool valid{false};
FsFileSystemHandle() = default;
~FsFileSystemHandle() {
if (valid)
fsFsClose(&fs);
} // NOLINT(modernize-use-equals-default)
FsFileSystemHandle(const FsFileSystemHandle&) = delete;
FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete;
FsFileSystem* get() {
return &fs;
}
void release() {
valid = false;
} // transfer ownership to devfs
};
// RAII wrapper for FILE* — auto-fclose on destruction.
struct FileHandle {
FILE* ptr{nullptr};
explicit FileHandle(FILE* file) : ptr(file) {}
~FileHandle() {
if (ptr != nullptr)
fclose(ptr);
} // NOLINT(modernize-use-equals-default)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
explicit operator bool() const {
return ptr != nullptr;
}
FILE* get() const {
return ptr;
}
};
// RAII wrapper for AccountProfile — auto-closes on destruction.
struct AccountProfileHandle {
AccountProfile profile{};
bool valid{false};
AccountProfileHandle() = default;
~AccountProfileHandle() {
if (valid)
accountProfileClose(&profile);
} // NOLINT(modernize-use-equals-default)
AccountProfileHandle(const AccountProfileHandle&) = delete;
AccountProfileHandle& operator=(const AccountProfileHandle&) = delete;
AccountProfile* get() {
return &profile;
}
};
} // namespace nxst
+19
View File
@@ -0,0 +1,19 @@
// Copyright (C) 2024-2026 NXST contributors
#pragma once
#include <string>
#include <switch.h>
#include <nxst/domain/result.hpp>
namespace io {
nxst::Result<std::string> backup(size_t index, AccountUid uid);
nxst::Result<std::string> restore(size_t index, AccountUid uid, const std::string& title_name);
Result copyDirectory(const std::string& src, const std::string& dst);
void copyFile(const std::string& src, const std::string& dst);
Result createDirectory(const std::string& path);
Result deleteFolderRecursively(const std::string& path);
bool directoryExists(const std::string& path);
bool fileExists(const std::string& path);
} // namespace io
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <unistd.h>
struct Socket {
int fd = -1;
Socket() = default;
explicit Socket(int fd) : fd(fd) {}
~Socket() {
if (fd >= 0)
close(fd);
}
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
Socket(Socket&& o) : fd(o.fd) {
o.fd = -1;
}
operator int() const {
return fd;
}
bool valid() const {
return fd >= 0;
}
void release() {
fd = -1;
}
};
+14
View File
@@ -0,0 +1,14 @@
#pragma once
// New API — use these going forward.
namespace nxst::log {
enum class Level { Debug, Info, Warn, Error };
void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
void info(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
void warn(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
void error(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
} // namespace nxst::log
+113
View File
@@ -0,0 +1,113 @@
#pragma once
#include <atomic>
#include <pthread.h>
#include <string>
#include <switch.h>
#include <nxst/domain/transfer_state.hpp>
namespace nxst {
class TransferService {
public:
int startSend(size_t title_index, AccountUid uid);
void cancelSend();
bool isSendDone() const {
return sender_state.done.load();
}
bool isSendCancelled() const {
return sender_state.cancelled.load();
}
bool isSendConnectionFailed() const {
return sender_state.connection_failed.load();
}
bool isSendProgressKnown() const {
return sender_state.bytes_total.load() > 0;
}
bool isSendWorkersIdle() const {
return !sender_active.load();
}
double sendProgress() const {
return sender_state.progress();
}
std::string sendStatusText() const {
return sender_state.getStatus();
}
std::string sendFailReason() const {
return sender_state.fail_reason;
}
int startReceive(size_t title_index, AccountUid uid, std::string title_name);
void cancelReceive();
bool isReceiveDone() const {
return receiver_state.done.load();
}
bool isReceiveCancelled() const {
return receiver_state.cancelled.load();
}
bool isReceiveWorkersIdle() const {
return !receiver_accept_active.load() && !receiver_broadcast_active.load();
}
double receiveProgress() const {
return receiver_state.progress();
}
std::string receiveStatusText() const {
return receiver_state.getStatus();
}
bool restoreSucceeded() const {
return restore_ok;
}
std::string restoreError() const {
return restore_error;
}
private:
// Sender
TransferState sender_state;
std::atomic<int> sender_udp_sock{-1};
std::atomic<int> sender_tcp_sock{-1};
std::atomic<bool> sender_active{false};
// Receiver
TransferState receiver_state;
std::atomic<int> receiver_client_sock{-1};
std::atomic<int> receiver_listen_sock{-1};
std::atomic<int> receiver_bcast_sock{-1};
std::atomic<bool> receiver_accept_active{false};
std::atomic<bool> receiver_broadcast_active{false};
pthread_t receiver_bcast_thread{};
// Stored at startReceive, read after network transfer completes
size_t restore_title_index{0};
AccountUid restore_uid{};
std::string restore_title_name;
bool restore_ok{false};
std::string restore_error;
// Sender thread
struct SenderArgs {
TransferService* svc;
size_t title_index;
AccountUid uid;
};
static void* senderEntry(void* arg);
void runSender(size_t title_index, AccountUid uid);
void failSend(const std::string& reason);
int findServer(char* out_ip);
// Receiver threads
struct AcceptArgs {
TransferService* svc;
int server_fd;
};
static void* broadcastEntry(void* arg);
static void* acceptEntry(void* arg);
void runBroadcast();
void runAccept(int server_fd);
std::string replaceUsername(const std::string& file_path) const;
};
} // namespace nxst
@@ -1,8 +1,8 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <Theme.hpp>
#include <ui/UiContext.hpp> #include <nxst/domain/account.hpp>
#include <account.hpp> #include <nxst/ui/theme.hpp>
namespace ui { namespace ui {
@@ -20,10 +20,8 @@ namespace ui {
HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") { HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") {
using namespace theme; using namespace theme;
bg = pu::ui::elm::Rectangle::New( bg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface);
0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface); divider = pu::ui::elm::Rectangle::New(0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider);
divider = pu::ui::elm::Rectangle::New(
0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider);
appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST"); appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST");
appName->SetFont(type::font(type::Title)); appName->SetFont(type::font(type::Title));
@@ -35,9 +33,7 @@ namespace ui {
const int chipW = 280; const int chipW = 280;
const int chipX = layout::ScreenW - chipW - space::lg; const int chipX = layout::ScreenW - chipW - space::lg;
chipBg = pu::ui::elm::Rectangle::New( chipBg = pu::ui::elm::Rectangle::New(chipX, 16, chipW, 40, color::BgSurface2, radius::pill);
chipX, 16, chipW, 40,
color::BgSurface2, radius::pill);
chipBg->SetVisible(false); chipBg->SetVisible(false);
avatar = pu::ui::elm::Image::New(chipX + 4, 20, ""); avatar = pu::ui::elm::Image::New(chipX + 4, 20, "");
@@ -65,7 +61,7 @@ namespace ui {
userName->SetVisible(show); userName->SetVisible(show);
if (show) { if (show) {
userName->SetText(name); userName->SetText(name);
std::string path = Account::iconPath(*uid); std::string path = account::iconPath(*uid);
if (!path.empty()) { if (!path.empty()) {
avatar->SetImage(path); avatar->SetImage(path);
avatar->SetWidth(32); avatar->SetWidth(32);
@@ -83,4 +79,4 @@ namespace ui {
subtitle->SetText(text); subtitle->SetText(text);
} }
}; };
} } // namespace ui
@@ -1,8 +1,10 @@
#pragma once #pragma once
#include <pu/Plutonium>
#include <Theme.hpp>
#include <vector>
#include <string> #include <string>
#include <vector>
#include <pu/Plutonium>
#include <nxst/ui/theme.hpp>
namespace ui { namespace ui {
@@ -21,19 +23,18 @@ namespace ui {
public: public:
HintBar(pu::ui::Layout* p) : parent(p) { HintBar(pu::ui::Layout* p) : parent(p) {
using namespace theme; using namespace theme;
bg = pu::ui::elm::Rectangle::New( bg = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW,
0, layout::ScreenH - layout::HintH, layout::HintH, color::BgSurface);
layout::ScreenW, layout::HintH, color::BgSurface); divider = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, 1,
divider = pu::ui::elm::Rectangle::New( color::Divider);
0, layout::ScreenH - layout::HintH,
layout::ScreenW, 1, color::Divider);
parent->Add(bg); parent->Add(bg);
parent->Add(divider); parent->Add(divider);
} }
void SetHints(const std::vector<Hint>& hints) { void SetHints(const std::vector<Hint>& hints) {
using namespace theme; using namespace theme;
for (auto& l : labels) l->SetVisible(false); for (auto& l : labels)
l->SetVisible(false);
labels.clear(); labels.clear();
int x = layout::ScreenW - space::lg; int x = layout::ScreenW - space::lg;
@@ -52,4 +53,4 @@ namespace ui {
} }
} }
}; };
} } // namespace ui
@@ -1,8 +1,9 @@
#pragma once #pragma once
#include <pu/Plutonium>
#include <string> #include <string>
#include <pu/Plutonium>
namespace theme { namespace theme {
using pu::ui::Color; using pu::ui::Color;
@@ -26,7 +27,7 @@ namespace theme {
constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF}; constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF};
constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF}; constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF};
} } // namespace color
namespace space { namespace space {
constexpr int xs = 4; constexpr int xs = 4;
@@ -35,14 +36,14 @@ namespace theme {
constexpr int lg = 24; constexpr int lg = 24;
constexpr int xl = 32; constexpr int xl = 32;
constexpr int xxl = 48; constexpr int xxl = 48;
} } // namespace space
namespace radius { namespace radius {
constexpr int sm = 6; constexpr int sm = 6;
constexpr int md = 12; constexpr int md = 12;
constexpr int lg = 20; constexpr int lg = 20;
constexpr int pill = 9999; constexpr int pill = 9999;
} } // namespace radius
namespace type { namespace type {
constexpr int Display = 38; constexpr int Display = 38;
@@ -54,7 +55,7 @@ namespace theme {
inline std::string font(int size) { inline std::string font(int size) {
return "DefaultFont@" + std::to_string(size); return "DefaultFont@" + std::to_string(size);
} }
} } // namespace type
namespace layout { namespace layout {
constexpr int ScreenW = 1280; constexpr int ScreenW = 1280;
@@ -63,16 +64,16 @@ namespace theme {
constexpr int HintH = 56; constexpr int HintH = 56;
constexpr int ContentTop = HeaderH; constexpr int ContentTop = HeaderH;
constexpr int ContentH = ScreenH - HeaderH - HintH; constexpr int ContentH = ScreenH - HeaderH - HintH;
} } // namespace layout
namespace motion { namespace motion {
constexpr int FadeFrames = 20; constexpr int FadeFrames = 20;
constexpr int SlideFrames = 14; constexpr int SlideFrames = 14;
constexpr int SpinnerFrames = 72; constexpr int SpinnerFrames = 72;
} } // namespace motion
namespace font { namespace font {
constexpr const char* Default = "Inter"; constexpr const char* Default = "Inter";
constexpr const char* Medium = "InterMedium"; constexpr const char* Medium = "InterMedium";
} } // namespace font
} } // namespace theme
@@ -1,12 +1,14 @@
#include <pu/Plutonium> #pragma once
#include <const.h> #include <memory>
#include <title.hpp>
#include <account.hpp>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <memory>
#include <ui/HeaderBar.hpp> #include <pu/Plutonium>
#include <ui/HintBar.hpp>
#include <nxst/domain/account.hpp>
#include <nxst/domain/title.hpp>
#include <nxst/ui/header_bar.hpp>
#include <nxst/ui/hint_bar.hpp>
namespace ui { namespace ui {
@@ -15,7 +17,6 @@ namespace ui {
class TitlesLayout : public pu::ui::Layout { class TitlesLayout : public pu::ui::Layout {
private: private:
pu::ui::elm::Menu::Ref titlesMenu; pu::ui::elm::Menu::Ref titlesMenu;
std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache; std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache;
bool m_inputLocked = false; bool m_inputLocked = false;
@@ -33,6 +34,7 @@ namespace ui {
pu::ui::elm::TextBlock::Ref emptyText; pu::ui::elm::TextBlock::Ref emptyText;
pu::ui::elm::TextBlock::Ref emptySub; pu::ui::elm::TextBlock::Ref emptySub;
AccountUid current_uid{};
TitlesFocus focus = TitlesFocus::List; TitlesFocus focus = TitlesFocus::List;
TitlesAction action = TitlesAction::Transfer; TitlesAction action = TitlesAction::Transfer;
int lockedListIndex = 0; int lockedListIndex = 0;
@@ -44,14 +46,17 @@ namespace ui {
void runReceive(int index, Title& title); void runReceive(int index, Title& title);
public: public:
TitlesLayout(); TitlesLayout();
void InitTitles(); void InitTitles(AccountUid uid);
void LockInput() { m_inputLocked = true; } void LockInput() {
void UnlockInput() { m_inputLocked = false; } m_inputLocked = true;
}
void UnlockInput() {
m_inputLocked = false;
}
void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos);
PU_SMART_CTOR(TitlesLayout) PU_SMART_CTOR(TitlesLayout)
}; };
} } // namespace ui
@@ -1,7 +1,8 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <Theme.hpp>
#include <util.hpp> #include <nxst/domain/util.hpp>
#include <nxst/ui/theme.hpp>
namespace ui { namespace ui {
@@ -21,23 +22,17 @@ namespace ui {
static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2; static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2;
public: public:
TransferOverlay(const std::string &title) TransferOverlay(const std::string& title)
: Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) : Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) {
{
using namespace theme; using namespace theme;
card = pu::ui::elm::Rectangle::New( card = pu::ui::elm::Rectangle::New(CardX, CardY, CardW, CardH, color::BgSurface, radius::lg);
CardX, CardY, CardW, CardH, color::BgSurface, radius::lg);
titleText = pu::ui::elm::TextBlock::New( titleText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg, title);
CardX + space::lg, CardY + space::lg, title);
titleText->SetFont(type::font(type::Title)); titleText->SetFont(type::font(type::Title));
titleText->SetColor(color::TextPrimary); titleText->SetColor(color::TextPrimary);
statusText = pu::ui::elm::TextBlock::New( statusText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg + 56, "");
CardX + space::lg,
CardY + space::lg + 56,
"");
statusText->SetFont(type::font(type::Body)); statusText->SetFont(type::font(type::Body));
statusText->SetColor(color::TextSecondary); statusText->SetColor(color::TextSecondary);
@@ -45,24 +40,19 @@ namespace ui {
int barY = CardY + space::lg + 56 + 56; int barY = CardY + space::lg + 56 + 56;
int barW = CardW - 2 * space::lg; int barW = CardW - 2 * space::lg;
progressTrack = pu::ui::elm::Rectangle::New( progressTrack = pu::ui::elm::Rectangle::New(barX, barY, barW, 8, color::Divider, radius::sm);
barX, barY, barW, 8, color::Divider, radius::sm);
progressBar = pu::ui::elm::ProgressBar::New( progressBar = pu::ui::elm::ProgressBar::New(barX, barY, barW, 8, 100.0);
barX, barY, barW, 8, 100.0);
progressBar->SetProgressColor(color::Primary); progressBar->SetProgressColor(color::Primary);
progressBar->SetBackgroundColor(color::Divider); progressBar->SetBackgroundColor(color::Divider);
indeterminateText = pu::ui::elm::TextBlock::New( indeterminateText = pu::ui::elm::TextBlock::New(barX, barY - 4, "Preparing transfer...");
barX, barY - 4, "Preparing transfer...");
indeterminateText->SetFont(type::font(type::Body)); indeterminateText->SetFont(type::font(type::Body));
indeterminateText->SetColor(color::TextMuted); indeterminateText->SetColor(color::TextMuted);
indeterminateText->SetVisible(false); indeterminateText->SetVisible(false);
hintText = pu::ui::elm::TextBlock::New( hintText =
CardX + space::lg, pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + CardH - space::lg - 18, "B to cancel");
CardY + CardH - space::lg - 18,
"B to cancel");
hintText->SetFont(type::font(type::Caption)); hintText->SetFont(type::font(type::Caption));
hintText->SetColor(color::TextMuted); hintText->SetColor(color::TextMuted);
@@ -76,8 +66,8 @@ namespace ui {
} }
PU_SMART_CTOR(TransferOverlay) PU_SMART_CTOR(TransferOverlay)
void SetStatus(const std::string &status) { void SetStatus(const std::string& status) {
statusText->SetText(StringUtils::elide(status, 56)); statusText->SetText(string_utils::elide(status, 56));
} }
void SetProgress(double val) { void SetProgress(double val) {
@@ -91,4 +81,4 @@ namespace ui {
} }
}; };
} } // namespace ui
@@ -1,14 +1,14 @@
#include <pu/Plutonium>
#include <const.h>
#include <ui/HeaderBar.hpp>
#include <ui/HintBar.hpp>
#include <memory> #include <memory>
#include <pu/Plutonium>
#include <nxst/ui/header_bar.hpp>
#include <nxst/ui/hint_bar.hpp>
namespace ui { namespace ui {
class UsersLayout : public pu::ui::Layout { class UsersLayout : public pu::ui::Layout {
private: private:
pu::ui::elm::Menu::Ref usersMenu; pu::ui::elm::Menu::Ref usersMenu;
pu::ui::elm::Rectangle::Ref loadingBg; pu::ui::elm::Rectangle::Ref loadingBg;
pu::ui::elm::TextBlock::Ref loadingText; pu::ui::elm::TextBlock::Ref loadingText;
@@ -16,7 +16,6 @@ namespace ui {
std::unique_ptr<HintBar> hints; std::unique_ptr<HintBar> hints;
public: public:
UsersLayout(); UsersLayout();
void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos);
@@ -24,6 +23,5 @@ namespace ui {
int32_t GetCurrentIndex(); int32_t GetCurrentIndex();
PU_SMART_CTOR(UsersLayout) PU_SMART_CTOR(UsersLayout)
}; };
} } // namespace ui
-17
View File
@@ -1,17 +0,0 @@
#pragma once
#include <cstdint>
namespace proto {
constexpr uint16_t TCP_PORT = 8080;
constexpr uint16_t MULTICAST_PORT = 8081;
constexpr char MULTICAST_GROUP[] = "239.0.0.1";
constexpr size_t BUF_SIZE = 65536;
constexpr uint32_t MAX_FILENAME = 4096;
constexpr uint32_t EOF_SENTINEL = 0;
// Wire layout per file:
// [filename_len : uint32_t LE] — 0 == end-of-stream
// [filename : filename_len bytes]
// [file_size : uint64_t LE]
// [file_data : file_size bytes]
}
-8
View File
@@ -1,8 +0,0 @@
#include <string>
int startSendingThread();
bool isServerTransferDone();
bool isServerTransferCancelled();
bool isServerWorkersIdle();
void cancelServerTransfer();
double getServerProgress();
std::string getServerStatusText();
-91
View File
@@ -1,91 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef TITLE_HPP
#define TITLE_HPP
#include "account.hpp"
#include "filesystem.hpp"
#include "io.hpp"
#include <algorithm>
#include <stdlib.h>
#include <string>
#include <switch.h>
#include <unordered_map>
#include <utility>
#include <vector>
class Title {
public:
void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name, const std::string& author);
~Title() = default;
std::string author(void);
std::pair<std::string, std::string> displayName(void);
u64 id(void);
std::string name(void);
std::string path(void);
u64 playTimeNanoseconds(void);
std::string playTime(void);
void playTimeNanoseconds(u64 playTimeNanoseconds);
u32 lastPlayedTimestamp(void);
void lastPlayedTimestamp(u32 lastPlayedTimestamp);
std::string fullPath(size_t index);
void refreshDirectories(void);
u64 saveId();
void saveId(u64 id);
std::vector<std::string> saves(void);
u8 saveDataType(void);
AccountUid userId(void);
std::string userName(void);
private:
u64 mId;
u64 mSaveId;
AccountUid mUserId;
std::string mUserName;
std::string mName;
std::string mSafeName;
std::string mAuthor;
std::string mPath;
std::vector<std::string> mSaves;
std::vector<std::string> mFullSavePaths;
u8 mSaveDataType;
std::pair<std::string, std::string> mDisplayName;
u64 mPlayTimeNanoseconds;
u32 mLastPlayedTimestamp;
};
void getTitle(Title& dst, AccountUid uid, size_t i);
size_t getTitleCount(AccountUid uid);
void loadTitles(void);
bool areTitlesLoaded(void);
void sortTitles(void);
void rotateSortMode(void);
void refreshDirectories(u64 id);
std::unordered_map<std::string, std::string> getCompleteTitleList(void);
#endif
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include <pu/Plutonium>
#include <Theme.hpp>
namespace ui {
class Card {
public:
pu::ui::elm::Rectangle::Ref bg;
Card(pu::ui::Layout* parent, int x, int y, int w, int h,
pu::ui::Color color = theme::color::BgSurface,
int rad = theme::radius::lg) {
bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad);
parent->Add(bg);
}
};
}
-12
View File
@@ -1,12 +0,0 @@
#pragma once
#include <string>
#include <optional>
#include <switch.h>
#include <account.hpp>
namespace ui {
struct UiContext {
std::optional<AccountUid> selectedUser;
std::string selectedUserName;
};
}
-53
View File
@@ -1,53 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#ifndef UTIL_HPP
#define UTIL_HPP
#include "account.hpp"
#include "common.hpp"
#include "io.hpp"
#include <switch.h>
#include <sys/stat.h>
// debug
#include <arpa/inet.h>
#include <sys/errno.h>
#include <sys/socket.h>
void servicesExit(void);
Result servicesInit(void);
HidsysNotificationLedPattern blinkLedPattern(u8 times);
void blinkLed(u8 times);
namespace StringUtils {
std::string removeAccents(std::string str);
std::string removeNotAscii(std::string str);
std::u16string UTF8toUTF16(const char* src);
std::string elide(const std::string& s, size_t maxChars);
}
#endif
Submodule lib deleted from b56564b70d
-21
View File
@@ -1,21 +0,0 @@
#include <string>
#include <switch.h>
#include <switch/services/hid.h>
#include <vector>
#include <MainApplication.hpp>
namespace ui {
MainApplication *mainApp;
void MainApplication::OnLoad() {
mainApp = this;
this->usersLayout = UsersLayout::New();
this->titlesLayout = TitlesLayout::New();
this->usersLayout->SetOnInput(
std::bind(&UsersLayout::onInput, this->usersLayout, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->titlesLayout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titlesLayout,std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->LoadLayout(this->usersLayout);
}
}
-142
View File
@@ -1,142 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#include "account.hpp"
#include <main.hpp>
#include <sys/stat.h>
#include <cstdio>
static std::map<AccountUid, User> mUsers;
Result Account::init(void)
{
Result res = accountInitialize(AccountServiceType_Application);
if (R_FAILED(res)) return res;
AccountUid uids[8];
s32 count = 0;
accountListAllUsers(uids, 8, &count);
for (s32 i = 0; i < count; i++) {
Account::username(uids[i]); // populates mUsers as side effect
}
return 0;
}
void Account::exit(void)
{
accountExit();
}
std::vector<AccountUid> Account::ids(void)
{
std::vector<AccountUid> v;
for (auto& value : mUsers) {
v.push_back(value.second.id);
}
return v;
}
static User getUser(AccountUid id)
{
User user{id, ""};
AccountProfile profile;
AccountProfileBase profilebase;
memset(&profilebase, 0, sizeof(profilebase));
if (R_SUCCEEDED(accountGetProfile(&profile, id))) {
if (R_SUCCEEDED(accountProfileGet(&profile, NULL, &profilebase))) {
user.name = std::string(profilebase.nickname);
}
accountProfileClose(&profile);
}
return user;
}
std::string Account::username(AccountUid id)
{
std::map<AccountUid, User>::const_iterator got = mUsers.find(id);
if (got == mUsers.end()) {
User user = getUser(id);
mUsers.insert({id, user});
return user.name;
}
return got->second.name;
}
std::string Account::iconPath(AccountUid id)
{
char path[128];
snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg",
id.uid[0], id.uid[1]);
struct stat st;
if (stat(path, &st) == 0 && st.st_size > 0) return std::string(path);
mkdir("sdmc:/switch", 0755);
mkdir("sdmc:/switch/NXST", 0755);
mkdir("sdmc:/switch/NXST/cache", 0755);
AccountProfile profile;
if (R_FAILED(accountGetProfile(&profile, id))) return "";
u32 imgSize = 0;
if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) {
accountProfileClose(&profile);
return "";
}
std::vector<u8> buf(imgSize);
u32 outSize = 0;
Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize);
accountProfileClose(&profile);
if (R_FAILED(r) || outSize == 0) return "";
FILE* f = fopen(path, "wb");
if (!f) return "";
fwrite(buf.data(), 1, outSize, f);
fclose(f);
return std::string(path);
}
AccountUid Account::selectAccount(void)
{
LibAppletArgs args;
libappletArgsCreate(&args, 0x10000);
u8 st_in[0xA0] = {0};
u8 st_out[0x18] = {0};
size_t repsz;
Result res = libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz);
if (R_SUCCEEDED(res)) {
u64 lres = *(u64*)st_out;
AccountUid uid = *(AccountUid*)&st_out[8];
if (lres == 0)
return uid;
}
return g_currentUId;
}
-249
View File
@@ -1,249 +0,0 @@
#include <arpa/inet.h>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <sys/select.h>
#include <fstream>
#include <iostream>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#ifdef __SWITCH__
#include <client.hpp>
#include <io.hpp>
#include <switch.h>
#endif
#include <protocol.hpp>
#include <TransferState.hpp>
namespace fs = std::filesystem;
using path = fs::path;
static TransferState g_client_state;
static std::atomic<int> g_client_udp_sock{-1};
static std::atomic<int> g_client_tcp_sock{-1};
static std::atomic<bool> g_client_thread_active{false};
bool isClientTransferDone() { return g_client_state.done.load(); }
bool isClientTransferCancelled() { return g_client_state.cancelled.load(); }
bool isClientConnectionFailed() { return g_client_state.connection_failed.load(); }
bool isClientProgressKnown() { return g_client_state.bytes_total.load() > 0; }
bool isClientWorkersIdle() { return !g_client_thread_active.load(); }
double getClientProgress() { return g_client_state.progress(); }
std::string getClientStatusText() { return g_client_state.getStatus(); }
std::string getClientFailReason() { return g_client_state.fail_reason; }
void cancelClientTransfer() {
g_client_state.cancelled.store(true);
int udp = g_client_udp_sock.exchange(-1);
if (udp >= 0) {
shutdown(udp, SHUT_RDWR);
close(udp);
}
int tcp = g_client_tcp_sock.exchange(-1);
if (tcp >= 0) {
shutdown(tcp, SHUT_RDWR);
close(tcp);
}
}
static bool send_all(int sock, const void* buf, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
if (n <= 0) return false;
sent += n;
}
return true;
}
static bool sendFile(int sock, const fs::path& filepath) {
std::ifstream infile(filepath, std::ios::binary | std::ios::ate);
if (!infile.is_open()) {
std::cerr << "File not found: " << filepath << std::endl;
return false;
}
uint32_t filename_len = (uint32_t)filepath.string().size();
uint64_t file_size = (uint64_t)infile.tellg();
infile.seekg(0, std::ios::beg);
std::cout << "Sending: " << filepath << " (" << file_size << " bytes)" << std::endl;
if (!send_all(sock, &filename_len, sizeof(filename_len))) return false;
if (!send_all(sock, filepath.c_str(), filename_len)) return false;
if (!send_all(sock, &file_size, sizeof(file_size))) return false;
std::vector<char> buffer(proto::BUF_SIZE);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
infile.read(buffer.data(), (std::streamsize)to_read);
std::streamsize count = infile.gcount();
if (count <= 0) break;
if (!send_all(sock, buffer.data(), (size_t)count)) {
std::cerr << "Failed to send data for: " << filepath << std::endl;
return false;
}
g_client_state.bytes_done.fetch_add((uint64_t)count);
remaining -= (uint64_t)count;
}
return true;
}
struct ThreadArgs { size_t index; AccountUid uid; };
static int find_server(char* server_ip);
static void fail_connect(const std::string& reason) {
g_client_state.fail_reason = reason;
g_client_state.connection_failed.store(true);
g_client_state.done.store(true);
}
static void* discovery_and_send_thread(void* arg) {
g_client_thread_active.store(true);
ThreadArgs* targs = static_cast<ThreadArgs*>(arg);
size_t index = targs->index;
AccountUid uid = targs->uid;
delete targs;
auto finish = [](void*) {
g_client_state.done.store(true);
g_client_thread_active.store(false);
return (void*)nullptr;
};
char server_ip[INET_ADDRSTRLEN];
if (find_server(server_ip) != 0) {
if (!g_client_state.cancelled.load())
fail_connect("No receiver found.\nMake sure the other Switch is in Receive mode.");
return finish(nullptr);
}
if (g_client_state.cancelled.load()) return finish(nullptr);
g_client_state.setStatus("Creating backup...");
auto backupResult = io::backup(index, uid);
if (!std::get<0>(backupResult)) {
fail_connect("Failed to create backup:\n" + std::get<2>(backupResult));
return finish(nullptr);
}
fs::path directory = std::get<2>(backupResult);
if (g_client_state.cancelled.load()) return finish(nullptr);
g_client_state.setStatus("Connecting...");
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_fd < 0) { fail_connect("Failed to open socket."); return finish(nullptr); }
g_client_tcp_sock.store(tcp_fd);
auto release_tcp = [&]() {
int owned = g_client_tcp_sock.exchange(-1);
if (owned == tcp_fd) close(tcp_fd);
};
sockaddr_in serv{};
serv.sin_family = AF_INET;
serv.sin_port = htons(proto::TCP_PORT);
if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 ||
connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) {
if (!g_client_state.cancelled.load())
fail_connect("Failed to connect to receiver.");
release_tcp();
return finish(nullptr);
}
uint64_t total = 0;
for (const auto& entry : fs::recursive_directory_iterator(directory))
if (fs::is_regular_file(entry.path()))
total += fs::file_size(entry.path());
g_client_state.bytes_total.store(total);
for (const auto& entry : fs::recursive_directory_iterator(directory)) {
if (g_client_state.cancelled.load()) break;
const path& p = entry.path();
if (fs::is_regular_file(p)) {
g_client_state.setStatus(p.filename().string());
if (!sendFile(tcp_fd, p)) break;
}
}
uint32_t sentinel = proto::EOF_SENTINEL;
send_all(tcp_fd, &sentinel, sizeof(sentinel));
release_tcp();
g_client_state.setStatus("");
return finish(nullptr);
}
static int find_server(char* server_ip) {
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_fd < 0) return -1;
g_client_udp_sock.store(udp_fd);
auto release_udp = [&]() {
int owned = g_client_udp_sock.exchange(-1);
if (owned == udp_fd) close(udp_fd);
};
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(proto::MULTICAST_PORT);
addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP);
const char* msg = "DISCOVER_SERVER";
if (sendto(udp_fd, msg, strlen(msg), 0, (sockaddr*)&addr, sizeof(addr)) < 0) {
release_udp();
return -1;
}
// Poll in 100ms slices so we can react to cancel within 100ms
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < deadline) {
if (g_client_state.cancelled.load()) {
release_udp();
return -1;
}
struct timeval tv{0, 100000};
fd_set fds;
FD_ZERO(&fds);
FD_SET(udp_fd, &fds);
if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) {
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
char buf[256];
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n > 0) {
buf[n] = '\0';
if (strcmp(buf, "SERVER_HERE") == 0) {
inet_ntop(AF_INET, &from.sin_addr, server_ip, INET_ADDRSTRLEN);
release_udp();
return 0;
}
}
}
}
release_udp();
return -1;
}
int transfer_files(size_t index, AccountUid uid) {
g_client_state.reset();
g_client_state.setStatus("Searching for receiver...");
ThreadArgs* arg = new ThreadArgs{index, uid};
pthread_t thread;
if (pthread_create(&thread, nullptr, discovery_and_send_thread, arg) != 0) {
delete arg;
return -1;
}
pthread_detach(thread);
return 0;
}
-124
View File
@@ -1,124 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#include "common.hpp"
std::string DateTime::timeStr(void)
{
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%02i:%02i:%02i", timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
}
std::string DateTime::dateTimeStr(void)
{
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour,
timeStruct.tm_min, timeStruct.tm_sec);
}
std::string DateTime::logDateTime(void)
{
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday,
timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
}
std::string StringUtils::UTF16toUTF8(const std::u16string& src)
{
static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
std::string dst = convert.to_bytes(src);
return dst;
}
std::string StringUtils::removeForbiddenCharacters(std::string src)
{
static const std::string illegalChars = ".,!\\/:?*\"<>|";
for (size_t i = 0, sz = src.length(); i < sz; i++) {
if (illegalChars.find(src[i]) != std::string::npos) {
src[i] = ' ';
}
}
size_t i;
for (i = src.length() - 1; i > 0 && src[i] == ' '; i--)
;
src.erase(i + 1, src.length() - i);
return src;
}
std::string StringUtils::format(const std::string fmt_str, ...)
{
va_list ap;
char* fp = NULL;
va_start(ap, fmt_str);
vasprintf(&fp, fmt_str.c_str(), ap);
va_end(ap);
std::unique_ptr<char[]> formatted(fp);
return std::string(formatted.get());
}
bool StringUtils::containsInvalidChar(const std::string& str)
{
for (size_t i = 0, sz = str.length(); i < sz; i++) {
if (!isascii(str[i])) {
return true;
}
}
return false;
}
void StringUtils::ltrim(std::string& s)
{
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
}
void StringUtils::rtrim(std::string& s)
{
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
}
void StringUtils::trim(std::string& s)
{
ltrim(s);
rtrim(s);
}
char* getConsoleIP(void)
{
struct in_addr in;
in.s_addr = gethostid();
return inet_ntoa(in);
}
-76
View File
@@ -1,76 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#include "directory.hpp"
Directory::Directory(const std::string& root)
{
mGood = false;
mError = 0;
mList.clear();
DIR* dir = opendir(root.c_str());
struct dirent* ent;
if (dir == NULL) {
mError = (Result)errno;
}
else {
while ((ent = readdir(dir))) {
std::string name = std::string(ent->d_name);
bool directory = ent->d_type == DT_DIR;
struct DirectoryEntry de = {name, directory};
mList.push_back(de);
}
closedir(dir);
mGood = true;
}
}
Result Directory::error(void)
{
return mError;
}
bool Directory::good(void)
{
return mGood;
}
std::string Directory::entry(size_t index)
{
return index < mList.size() ? mList.at(index).name : "";
}
bool Directory::folder(size_t index)
{
return index < mList.size() ? mList.at(index).directory : false;
}
size_t Directory::size(void)
{
return mList.size();
}
-42
View File
@@ -1,42 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#include "filesystem.hpp"
Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID)
{
return fsOpen_SaveData(fileSystem, titleID, userID);
}
int FileSystem::mount(FsFileSystem fs)
{
return fsdevMountDevice("save", fs);
}
void FileSystem::unmount(void)
{
fsdevUnmountDevice("save");
}
-333
View File
@@ -1,333 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#include "io.hpp"
#include "main.hpp"
#include <logger.hpp>
bool io::fileExists(const std::string& path)
{
struct stat buffer;
return (stat(path.c_str(), &buffer) == 0);
}
void io::copyFile(const std::string& srcPath, const std::string& dstPath)
{
g_isTransferringFile = true;
FILE* src = fopen(srcPath.c_str(), "rb");
if (src == NULL) {
Logger::getInstance().log(Logger::ERROR, "Failed to open source file " + srcPath + " during copy with errno %d. Skipping...", errno);
return;
}
FILE* dst = fopen(dstPath.c_str(), "wb");
if (dst == NULL) {
Logger::getInstance().log(Logger::ERROR, "Failed to open destination file " + dstPath + " during copy with errno " + std::to_string(errno) + ". Skipping...");
fclose(src);
return;
}
fseek(src, 0, SEEK_END);
u64 sz = ftell(src);
rewind(src);
u8* buf = new u8[BUFFER_SIZE];
u64 offset = 0;
size_t slashpos = srcPath.rfind("/");
g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1);
while (offset < sz) {
u32 count = fread((char*)buf, 1, BUFFER_SIZE, src);
if (count == 0) {
Logger::getInstance().log(Logger::ERROR, "fread returned 0 for file {} at offset {}/{} with errno {}. Aborting copy.", srcPath, offset, sz, errno);
break;
}
fwrite((char*)buf, 1, count, dst);
offset += count;
}
delete[] buf;
fclose(src);
fclose(dst);
if (dstPath.rfind("save:/", 0) == 0) {
fsdevCommitDevice("save");
}
g_isTransferringFile = false;
}
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
{
Result res = 0;
bool quit = false;
Directory items(srcPath);
if (!items.good()) {
return items.error();
}
for (size_t i = 0, sz = items.size(); i < sz && !quit; i++) {
std::string newsrc = srcPath + items.entry(i);
std::string newdst = dstPath + items.entry(i);
if (items.folder(i)) {
res = io::createDirectory(newdst);
if (R_SUCCEEDED(res)) {
newsrc += "/";
newdst += "/";
res = io::copyDirectory(newsrc, newdst);
}
else {
quit = true;
}
}
else {
io::copyFile(newsrc, newdst);
}
}
return 0;
}
Result io::createDirectory(const std::string& path)
{
mkdir(path.c_str(), 777);
return 0;
}
bool io::directoryExists(const std::string& path)
{
struct stat sb;
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
}
Result io::deleteFolderRecursively(const std::string& path)
{
Directory dir(path);
if (!dir.good()) {
return dir.error();
}
for (size_t i = 0, sz = dir.size(); i < sz; i++) {
if (dir.folder(i)) {
std::string newpath = path + "/" + dir.entry(i) + "/";
deleteFolderRecursively(newpath);
newpath = path + dir.entry(i);
rmdir(newpath.c_str());
}
else {
std::string newpath = path + dir.entry(i);
std::remove(newpath.c_str());
}
}
rmdir(path.c_str());
return 0;
}
std::tuple<bool, Result, std::string> io::backup(size_t index, AccountUid uid)
{
Result res = 0;
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
Title title;
getTitle(title, uid, index);
Logger::getInstance().log(Logger::INFO, "Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
title.userId().uid[1], title.userId().uid[0]);
FsFileSystem fileSystem;
res = FileSystem::mount(&fileSystem, title.id(), title.userId());
if (R_SUCCEEDED(res)) {
int rc = FileSystem::mount(fileSystem);
if (rc == -1) {
fsFsClose(&fileSystem);
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
title.userId().uid[1], title.userId().uid[0]);
return std::make_tuple(false, -2, "Failed to mount save.");
}
}
else {
Logger::getInstance().log(Logger::ERROR,
"Failed to mount filesystem during backup with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
title.userId().uid[1], title.userId().uid[0]);
return std::make_tuple(false, res, "Failed to mount save.");
}
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
io::createDirectory(title.path());
std::string dstPath = title.path() + "/" + suggestion;
// Write to a temp dir first; rename on success so the existing backup
// is never destroyed if the copy is interrupted mid-way.
std::string tmpPath = dstPath + ".tmp";
if (io::directoryExists(tmpPath)) {
io::deleteFolderRecursively((tmpPath + "/").c_str());
}
io::createDirectory(tmpPath);
res = io::copyDirectory("save:/", tmpPath + "/");
if (R_FAILED(res)) {
FileSystem::unmount();
io::deleteFolderRecursively((tmpPath + "/").c_str());
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory to " + tmpPath + " with result 0x%08lX.", res);
return std::make_tuple(false, res, "Failed to backup save.");
}
// Swap: delete old backup only after new one is fully written.
if (io::directoryExists(dstPath)) {
io::deleteFolderRecursively((dstPath + "/").c_str());
}
if (rename(tmpPath.c_str(), dstPath.c_str()) != 0) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to rename temp backup to " + dstPath);
return std::make_tuple(false, (Result)-1, "Failed to finalise backup.");
}
refreshDirectories(title.id());
FileSystem::unmount();
ret = std::make_tuple(true, 0, dstPath);
Logger::getInstance().log(Logger::INFO, "Backup succeeded.");
return ret;
}
std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell)
{
Result res = 0;
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
Title title;
getTitle(title, uid, index);
Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
title.userId().uid[1], title.userId().uid[0]);
// Если сейв ещё не существует (игра не запускалась) — создаём его через NACP.
// fsCreateSaveDataFileSystem возвращает ошибку если сейв уже есть — это нормально.
{
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
if (nsacd != NULL) {
memset(nsacd, 0, sizeof(NsApplicationControlData));
size_t outsize = 0;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title.id(), nsacd, sizeof(NsApplicationControlData), &outsize))) {
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
FsSaveDataAttribute attr = {};
attr.application_id = title.id();
attr.uid = uid;
attr.save_data_type = FsSaveDataType_Account;
attr.save_data_rank = FsSaveDataRank_Primary;
FsSaveDataCreationInfo createInfo = {};
createInfo.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
createInfo.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
createInfo.available_size = 0x4000;
createInfo.owner_id = nsacd->nacp.save_data_owner_id;
createInfo.save_data_space_id = FsSaveDataSpaceId_User;
fsCreateSaveDataFileSystem(&attr, &createInfo, &meta);
}
free(nsacd);
}
}
FsFileSystem fileSystem;
res = FileSystem::mount(&fileSystem, title.id(), uid);
if (R_SUCCEEDED(res)) {
int rc = FileSystem::mount(fileSystem);
if (rc == -1) {
fsFsClose(&fileSystem);
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
uid.uid[1], uid.uid[0]);
return std::make_tuple(false, -2, "Failed to mount save.");
}
}
else {
Logger::getInstance().log(Logger::ERROR,
"Failed to mount filesystem during restore with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
uid.uid[1], uid.uid[0]);
return std::make_tuple(false, res, "Failed to mount save.");
}
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid)));
std::string srcPath = title.path() + "/" + suggestion + "/";
std::string dstPath = "save:/";
// Validate source exists and is non-empty before touching live save data.
{
Directory srcCheck(srcPath);
if (!srcCheck.good() || srcCheck.size() == 0) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Restore source is empty or missing: " + srcPath);
return std::make_tuple(false, (Result)-1, "Restore source is empty or missing.");
}
}
{
Directory saveRoot(dstPath);
for (size_t i = 0, sz = saveRoot.size(); i < sz; i++) {
if (saveRoot.folder(i)) {
io::deleteFolderRecursively((dstPath + saveRoot.entry(i) + "/").c_str());
rmdir((dstPath + saveRoot.entry(i)).c_str());
} else {
std::remove((dstPath + saveRoot.entry(i)).c_str());
}
}
}
res = fsdevCommitDevice("save");
if (R_FAILED(res)) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to commit save after clearing with result 0x%08lX.", res);
return std::make_tuple(false, res, "Failed to commit save after delete.");
}
res = io::copyDirectory(srcPath, dstPath);
if (R_FAILED(res)) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + srcPath + " to " + dstPath + " with result 0x%08lX. Skipping...", res);
return std::make_tuple(false, res, "Failed to restore save.");
}
res = fsdevCommitDevice("save");
if (R_FAILED(res)) {
Logger::getInstance().log(Logger::ERROR, "Failed to commit save with result 0x%08lX.", res);
return std::make_tuple(false, res, "Failed to commit to save device.");
}
else {
blinkLed(4);
ret = std::make_tuple(true, 0, nameFromCell + "\nhas been restored successfully.");
}
FileSystem::unmount();
Logger::getInstance().log(Logger::INFO, "Restore succeeded.");
return ret;
}
-345
View File
@@ -1,345 +0,0 @@
#include <arpa/inet.h>
#include <atomic>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <netinet/in.h>
#include <pthread.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#ifdef __SWITCH__
#include <server.hpp>
#include <switch.h>
#include <main.hpp>
#endif
#include <protocol.hpp>
#include <TransferState.hpp>
#include <net/Socket.hpp>
static TransferState g_server_state;
static std::atomic<int> g_server_client_sock{-1};
static std::atomic<int> g_server_listen_sock{-1};
static std::atomic<int> g_broadcast_sock{-1};
static std::atomic<bool> g_accept_thread_active{false};
static std::atomic<bool> g_broadcast_thread_active{false};
static pthread_t g_broadcast_thread{};
bool isServerTransferDone() { return g_server_state.done.load(); }
bool isServerTransferCancelled() { return g_server_state.cancelled.load(); }
bool isServerWorkersIdle() { return !g_accept_thread_active.load() && !g_broadcast_thread_active.load(); }
double getServerProgress() { return g_server_state.progress(); }
std::string getServerStatusText() { return g_server_state.getStatus(); }
void cancelServerTransfer() {
g_server_state.cancelled.store(true);
int sock = g_server_client_sock.exchange(-1);
if (sock >= 0) {
shutdown(sock, SHUT_RDWR);
close(sock);
}
int lsock = g_server_listen_sock.exchange(-1);
if (lsock >= 0) {
shutdown(lsock, SHUT_RDWR);
close(lsock);
}
int bsock = g_broadcast_sock.exchange(-1);
if (bsock >= 0) {
shutdown(bsock, SHUT_RDWR);
close(bsock);
}
if (g_broadcast_thread_active.load()) {
pthread_cancel(g_broadcast_thread);
}
}
#ifdef __SWITCH__
static std::string replaceUsername(const std::string& path) {
std::string username = StringUtils::removeNotAscii(
StringUtils::removeAccents(Account::username(g_currentUId)));
size_t lastSlash = path.rfind('/');
if (lastSlash == std::string::npos) return path;
size_t prevSlash = path.rfind('/', lastSlash - 1);
if (prevSlash == std::string::npos)
return username + path.substr(lastSlash);
return path.substr(0, prevSlash + 1) + username + path.substr(lastSlash);
}
#endif
static bool recv_all(int sock, void* buf, size_t len) {
size_t received = 0;
while (received < len) {
ssize_t n = read(sock, static_cast<char*>(buf) + received, len - received);
if (n <= 0) return false;
received += n;
}
return true;
}
static void mkdirs(const std::string& path) {
for (size_t i = 1; i < path.size(); i++) {
if (path[i] == '/') {
std::string component = path.substr(0, i);
mkdir(component.c_str(), 0777);
}
}
mkdir(path.c_str(), 0777);
}
static void receive_file(int sock, const std::string& relative_path, uint64_t file_size) {
std::cout << "Receiving: " << relative_path << " (" << file_size << " bytes)" << std::endl;
size_t last_slash = relative_path.rfind('/');
if (last_slash != std::string::npos) {
std::string dir = relative_path.substr(0, last_slash);
if (!dir.empty()) mkdirs(dir);
}
FILE* outfile = fopen(relative_path.c_str(), "wb");
if (!outfile) {
std::cerr << "Failed to open for writing: " << relative_path
<< " errno=" << errno << std::endl;
// Drain so sender doesn't hang
std::vector<char> drain(proto::BUF_SIZE);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, drain.data(), to_read);
if (n <= 0) break;
remaining -= (uint64_t)n;
}
return;
}
g_server_state.bytes_total.store(file_size);
g_server_state.bytes_done.store(0);
std::vector<char> buffer(proto::BUF_SIZE);
uint64_t total_received = 0;
while (total_received < file_size) {
size_t to_read = (size_t)std::min(file_size - total_received, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, buffer.data(), to_read);
if (n <= 0) {
std::cerr << "Read error receiving: " << relative_path << std::endl;
break;
}
fwrite(buffer.data(), 1, (size_t)n, outfile);
total_received += (uint64_t)n;
g_server_state.bytes_done.store(total_received);
}
fclose(outfile);
std::cout << "Received: " << relative_path << std::endl;
}
static void* handle_client(void* socket_desc) {
int client_socket = *(int*)socket_desc;
delete static_cast<int*>(socket_desc);
while (true) {
uint32_t filename_len = 0;
if (!recv_all(client_socket, &filename_len, sizeof(filename_len)))
break;
if (filename_len == proto::EOF_SENTINEL) {
std::cout << "End of transfer." << std::endl;
break;
}
if (filename_len > proto::MAX_FILENAME) {
std::cerr << "filename_len=" << filename_len << " exceeds MAX_FILENAME, aborting." << std::endl;
break;
}
std::vector<char> filename(filename_len + 1, '\0');
if (!recv_all(client_socket, filename.data(), filename_len)) {
std::cerr << "Short read on filename, aborting." << std::endl;
break;
}
std::string filename_str(filename.data(), filename_len);
#ifdef __SWITCH__
filename_str = replaceUsername(filename_str);
#endif
{
size_t sl = filename_str.rfind('/');
g_server_state.setStatus(
sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str);
}
uint64_t file_size = 0;
if (!recv_all(client_socket, &file_size, sizeof(file_size))) {
std::cerr << "Short read on file_size, aborting." << std::endl;
break;
}
receive_file(client_socket, filename_str, file_size);
}
int owned_client = g_server_client_sock.exchange(-1);
if (owned_client == client_socket) {
close(client_socket);
}
return nullptr;
}
struct AcceptArgs { int server_fd; };
static void* accept_and_handle(void* arg) {
g_accept_thread_active.store(true);
int server_fd = static_cast<AcceptArgs*>(arg)->server_fd;
delete static_cast<AcceptArgs*>(arg);
g_server_listen_sock.store(server_fd);
sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int client_socket = accept(server_fd, (sockaddr*)&client_addr, &client_len);
int owned_listen = g_server_listen_sock.exchange(-1);
if (owned_listen == server_fd) {
close(server_fd);
}
if (client_socket >= 0) {
g_server_client_sock.store(client_socket);
int* pclient = new (std::nothrow) int(client_socket);
if (pclient) {
handle_client(pclient);
} else {
close(client_socket);
}
}
g_server_state.done.store(true);
g_accept_thread_active.store(false);
return nullptr;
}
static void* broadcast_listener(void* arg) {
g_broadcast_thread_active.store(true);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
int udp = socket(AF_INET, SOCK_DGRAM, 0);
if (udp < 0) {
perror("broadcast_listener: socket");
g_broadcast_thread_active.store(false);
return nullptr;
}
g_broadcast_sock.store(udp);
struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit
setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(proto::MULTICAST_PORT);
if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) {
perror("broadcast_listener: bind");
int owned = g_broadcast_sock.exchange(-1);
if (owned == udp) close(udp);
g_broadcast_thread_active.store(false);
return nullptr;
}
ip_mreq group{};
group.imr_multiaddr.s_addr = inet_addr(proto::MULTICAST_GROUP);
group.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) {
perror("broadcast_listener: setsockopt");
int owned = g_broadcast_sock.exchange(-1);
if (owned == udp) close(udp);
g_broadcast_thread_active.store(false);
return nullptr;
}
std::cout << "Broadcast listener started" << std::endl;
char buf[256];
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
while (true) {
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n < 0) {
if (g_server_state.cancelled.load()) break;
continue;
}
buf[n] = '\0';
if (strcmp(buf, "DISCOVER_SERVER") == 0) {
const char* reply = "SERVER_HERE";
sendto(udp, reply, strlen(reply), 0, (sockaddr*)&from, fromlen);
std::cout << "Discovery replied." << std::endl;
break;
}
}
int owned = g_broadcast_sock.exchange(-1);
if (owned == udp) close(udp);
g_broadcast_thread_active.store(false);
return nullptr;
}
int startSendingThread() {
g_server_state.reset();
g_server_state.setStatus("Waiting for connection...");
pthread_t broadcast_thread;
if (pthread_create(&broadcast_thread, nullptr, broadcast_listener, nullptr) != 0) {
perror("startSendingThread: broadcast thread");
return 1;
}
g_broadcast_thread = broadcast_thread;
pthread_detach(broadcast_thread);
Socket server(socket(AF_INET, SOCK_STREAM, 0));
if (!server.valid()) {
perror("startSendingThread: socket");
cancelServerTransfer();
return 1;
}
int yes = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(proto::TCP_PORT);
if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0) {
perror("startSendingThread: bind");
cancelServerTransfer();
return 1;
}
if (listen(server, 3) < 0) {
perror("startSendingThread: listen");
cancelServerTransfer();
return 1;
}
AcceptArgs* acc_args = new AcceptArgs{server.fd};
pthread_t accept_thread;
if (pthread_create(&accept_thread, nullptr, accept_and_handle, acc_args) != 0) {
delete acc_args;
cancelServerTransfer();
return 1;
}
pthread_detach(accept_thread);
server.release(); // accepted by accept_and_handle
return 0;
}
#ifndef __SWITCH__
int main() {
if (startSendingThread() != 0) return 1;
while (!isServerTransferDone()) usleep(16000);
return 0;
}
#endif
-323
View File
@@ -1,323 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#include "title.hpp"
#include "main.hpp"
static std::unordered_map<AccountUid, std::vector<Title>> titles;
static bool s_titlesLoaded = false;
bool areTitlesLoaded(void)
{
return s_titlesLoaded;
}
void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, const std::string& author)
{
mId = id;
mUserId = userID;
mSaveDataType = saveDataType;
mUserName = Account::username(userID);
mAuthor = author;
mName = name;
mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId) : StringUtils::removeForbiddenCharacters(name);
mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName;
std::string aname = StringUtils::removeAccents(mName);
size_t pos = aname.rfind(":");
mDisplayName = std::make_pair(aname, "");
if (pos != std::string::npos) {
std::string name1 = aname.substr(0, pos);
std::string name2 = aname.substr(pos + 1);
StringUtils::trim(name1);
StringUtils::trim(name2);
mDisplayName.first = name1;
mDisplayName.second = name2;
}
else {
// check for parenthesis
size_t pos1 = aname.rfind("(");
size_t pos2 = aname.rfind(")");
if (pos1 != std::string::npos && pos2 != std::string::npos) {
std::string name1 = aname.substr(0, pos1);
std::string name2 = aname.substr(pos1 + 1, pos2 - 1 - pos1);
StringUtils::trim(name1);
StringUtils::trim(name2);
mDisplayName.first = name1;
mDisplayName.second = name2;
}
}
refreshDirectories();
}
u8 Title::saveDataType(void)
{
return mSaveDataType;
}
u64 Title::id(void)
{
return mId;
}
u64 Title::saveId(void)
{
return mSaveId;
}
void Title::saveId(u64 saveId)
{
mSaveId = saveId;
}
AccountUid Title::userId(void)
{
return mUserId;
}
std::string Title::userName(void)
{
return mUserName;
}
std::string Title::author(void)
{
return mAuthor;
}
std::string Title::name(void)
{
return mName;
}
std::pair<std::string, std::string> Title::displayName(void)
{
return mDisplayName;
}
std::string Title::path(void)
{
return mPath;
}
std::string Title::fullPath(size_t index)
{
return mFullSavePaths.at(index);
}
std::vector<std::string> Title::saves()
{
return mSaves;
}
u64 Title::playTimeNanoseconds(void)
{
return mPlayTimeNanoseconds;
}
std::string Title::playTime(void)
{
const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000;
return StringUtils::format("%d", playTimeMinutes / 60) + ":" + StringUtils::format("%02d", playTimeMinutes % 60) + " hours";
}
void Title::playTimeNanoseconds(u64 playTimeNanoseconds)
{
mPlayTimeNanoseconds = playTimeNanoseconds;
}
u32 Title::lastPlayedTimestamp(void)
{
return mLastPlayedTimestamp;
}
void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp)
{
mLastPlayedTimestamp = lastPlayedTimestamp;
}
void Title::refreshDirectories(void)
{
mSaves.clear();
mFullSavePaths.clear();
Directory savelist(mPath);
if (savelist.good()) {
for (size_t i = 0, sz = savelist.size(); i < sz; i++) {
if (savelist.folder(i)) {
mSaves.push_back(savelist.entry(i));
mFullSavePaths.push_back(mPath + "/" + savelist.entry(i));
}
}
std::sort(mSaves.rbegin(), mSaves.rend());
std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend());
mSaves.insert(mSaves.begin(), g_emptySave);
mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave);
}
else {
Logger::getInstance().log(Logger::ERROR, "Couldn't retrieve the extdata directory list for the title " + name());
}
}
void loadTitles(void)
{
if (s_titlesLoaded) return;
s_titlesLoaded = true;
titles.clear();
FsSaveDataInfoReader reader;
FsSaveDataInfo info;
s64 total_entries = 0;
size_t outsize = 0;
NacpLanguageEntry* nle = NULL;
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
if (nsacd == NULL) {
return;
}
memset(nsacd, 0, sizeof(NsApplicationControlData));
Result res = fsOpenSaveDataInfoReader(&reader, FsSaveDataSpaceId_User);
if (R_FAILED(res)) {
free(nsacd);
return;
}
while (1) {
res = fsSaveDataInfoReaderRead(&reader, &info, 1, &total_entries);
if (R_FAILED(res) || total_entries == 0) {
break;
}
if (info.save_data_type == FsSaveDataType_Account) {
u64 tid = info.application_id;
u64 sid = info.save_data_id;
AccountUid uid = info.uid;
// if (mFilterIds.find(tid) == mFilterIds.end()) {
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, sizeof(NsApplicationControlData), &outsize);
if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) {
res = nacpGetLanguageEntry(&nsacd->nacp, &nle);
if (R_SUCCEEDED(res) && nle != NULL) {
Title title;
title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author));
title.saveId(sid);
// load play statistics
PdmPlayStatistics stats;
res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats);
if (R_SUCCEEDED(res)) {
title.playTimeNanoseconds(stats.playtime);
title.lastPlayedTimestamp(stats.last_timestamp_user);
}
// loadIcon(tid, nsacd, outsize - sizeof(nsacd->nacp));
// check if the vector is already created
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
if (it != titles.end()) {
// found
it->second.push_back(title);
}
else {
// not found, insert into map
std::vector<Title> v;
v.push_back(title);
titles.emplace(uid, v);
}
}
}
nle = NULL;
// }
}
}
free(nsacd);
fsSaveDataInfoReaderClose(&reader);
sortTitles();
}
void sortTitles(void)
{
for (auto& vect : titles) {
std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) {
switch (g_sortMode) {
case SORT_LAST_PLAYED:
return l.lastPlayedTimestamp() > r.lastPlayedTimestamp();
case SORT_PLAY_TIME:
return l.playTimeNanoseconds() > r.playTimeNanoseconds();
case SORT_ALPHA:
default:
return l.name() < r.name();
}
});
}
}
void rotateSortMode(void)
{
g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT);
sortTitles();
}
void getTitle(Title& dst, AccountUid uid, size_t i)
{
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
if (it != titles.end() && i < getTitleCount(uid)) {
dst = it->second.at(i);
}
}
size_t getTitleCount(AccountUid uid)
{
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
return it != titles.end() ? it->second.size() : 0;
}
void refreshDirectories(u64 id)
{
for (auto& pair : titles) {
for (size_t i = 0; i < pair.second.size(); i++) {
if (pair.second.at(i).id() == id) {
pair.second.at(i).refreshDirectories();
}
}
}
}
std::unordered_map<std::string, std::string> getCompleteTitleList(void)
{
std::unordered_map<std::string, std::string> map;
for (const auto& pair : titles) {
for (auto value : pair.second) {
map.insert({StringUtils::format("0x%016llX", value.id()), value.name()});
}
}
return map;
}
-175
View File
@@ -1,175 +0,0 @@
/*
* This file is part of Checkpoint
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
* * Requiring preservation of specified reasonable legal notices or
* author attributions in that material or in the Appropriate Legal
* Notices displayed by works containing it.
* * Prohibiting misrepresentation of the origin of that material,
* or requiring that modified versions of such material be marked in
* reasonable ways as different from the original version.
*/
#include "util.hpp"
#include <logger.hpp>
#include <MainApplication.hpp>
#include "main.hpp"
void servicesExit(void)
{
Logger::getInstance().flush();
Account::exit();
plExit();
romfsExit();
}
Result servicesInit(void)
{
io::createDirectory("sdmc:/switch");
io::createDirectory("sdmc:/switch/NXST");
io::createDirectory("sdmc:/switch/NXST/saves");
if (appletGetAppletType() != AppletType_Application) {
Logger::getInstance().log(Logger::WARN, "Please do not run NXST in applet mode.");
}
// Result socinit = 0;
// if ((socinit = socketInitializeDefault()) == 0) {
// nxlinkStdio();
// }
// else {
// Logger::getInstance().log(Logger::INFO, "Unable to initialize socket. Result code 0x%08lX.", socinit);
// }
// g_shouldExitNetworkLoop = R_FAILED(socinit);
Result res = 0;
romfsInit();
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
hidInitializeTouchScreen();
if (R_FAILED(res = plInitialize(PlServiceType_User))) {
Logger::getInstance().log(Logger::ERROR, "plInitialize failed. Result code 0x%08lX.", res);
return res;
}
if (R_FAILED(res = Account::init())) {
Logger::getInstance().log(Logger::ERROR, "Account::init failed. Result code 0x%08lX.", res);
return res;
}
if (R_FAILED(res = nsInitialize())) {
Logger::getInstance().log(Logger::ERROR, "nsInitialize failed. Result code 0x{:08X}.", res);
return res;
}
if (R_SUCCEEDED(res = hidsysInitialize())) {
g_notificationLedAvailable = true;
}
else {
Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res);
}
Logger::getInstance().log(Logger::INFO, "NXST loading completed!");
return 0;
}
std::u16string StringUtils::UTF8toUTF16(const char* src)
{
char16_t tmp[256] = {0};
utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256);
return std::u16string(tmp);
}
// https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c
std::string StringUtils::removeAccents(std::string str)
{
std::u16string src = UTF8toUTF16(str.c_str());
const std::u16string illegal = UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ");
const std::u16string fixed = UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy");
for (size_t i = 0, sz = src.length(); i < sz; i++) {
size_t index = illegal.find(src[i]);
if (index != std::string::npos) {
src[i] = fixed[index];
}
}
return UTF16toUTF8(src);
}
std::string StringUtils::removeNotAscii(std::string str)
{
for (size_t i = 0, sz = str.length(); i < sz; i++) {
if (!isascii(str[i])) {
str[i] = ' ';
}
}
return str;
}
std::string StringUtils::elide(const std::string& s, size_t maxChars)
{
if (s.size() <= maxChars || maxChars < 6) return s;
constexpr const char* dots = "...";
size_t budget = maxChars - 3;
size_t head = (budget + 1) / 2;
size_t tail = budget - head;
return s.substr(0, head) + dots + s.substr(s.size() - tail);
}
HidsysNotificationLedPattern blinkLedPattern(u8 times)
{
HidsysNotificationLedPattern pattern;
memset(&pattern, 0, sizeof(pattern));
pattern.baseMiniCycleDuration = 0x1; // 12.5ms.
pattern.totalMiniCycles = 0x2; // 2 mini cycles.
pattern.totalFullCycles = times; // Repeat n times.
pattern.startIntensity = 0x0; // 0%.
pattern.miniCycles[0].ledIntensity = 0xF; // 100%.
pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms.
pattern.miniCycles[1].ledIntensity = 0x0; // 0%.
pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms.
return pattern;
}
void blinkLed(u8 times)
{
if (g_notificationLedAvailable) {
PadState pad;
padInitializeDefault(&pad);
s32 n;
HidsysUniquePadId uniquePadIds[2] = {0};
HidsysNotificationLedPattern pattern = blinkLedPattern(times);
memset(uniquePadIds, 0, sizeof(uniquePadIds));
Result res = hidsysGetUniquePadsFromNpad(padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n);
if (R_SUCCEEDED(res)) {
for (s32 i = 0; i < n; i++) {
hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]);
}
}
}
}
+16 -10
View File
@@ -1,10 +1,12 @@
#include <MainApplication.hpp>
#include "util.hpp"
#include "main.hpp"
#include <server.hpp>
#include <client.hpp>
#include <unistd.h> #include <unistd.h>
#include <nxst/app/main_application.hpp>
#include <nxst/domain/util.hpp>
namespace ui {
extern MainApplication* mainApp;
}
static int nxlink_sock = -1; static int nxlink_sock = -1;
extern "C" void userAppInit() { extern "C" void userAppInit() {
@@ -22,11 +24,15 @@ extern "C" void userAppInit() {
} }
extern "C" void userAppExit() { extern "C" void userAppExit() {
cancelServerTransfer(); if (ui::mainApp) {
cancelClientTransfer(); ui::mainApp->transfer.cancelReceive();
for (int i = 0; i < 150 && (!isServerWorkersIdle() || !isClientWorkersIdle()); i++) { ui::mainApp->transfer.cancelSend();
for (int i = 0; i < 150 && (!ui::mainApp->transfer.isReceiveWorkersIdle() ||
!ui::mainApp->transfer.isSendWorkersIdle());
i++) {
usleep(10000); usleep(10000);
} }
}
if (nxlink_sock != -1) { if (nxlink_sock != -1) {
close(nxlink_sock); close(nxlink_sock);
} }
@@ -53,8 +59,8 @@ int main() {
// First create our renderer, where one can customize SDL or other stuff's // First create our renderer, where one can customize SDL or other stuff's
// initialization. // initialization.
auto renderer_opts = pu::ui::render::RendererInitOptions( auto renderer_opts =
SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); pu::ui::render::RendererInitOptions(SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags);
renderer_opts.UseImage(pu::ui::render::IMGAllFlags); renderer_opts.UseImage(pu::ui::render::IMGAllFlags);
renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); renderer_opts.UseAudio(pu::ui::render::MixerAllFlags);
renderer_opts.UseTTF(); renderer_opts.UseTTF();
+20
View File
@@ -0,0 +1,20 @@
#include <switch.h>
#include <nxst/app/main_application.hpp>
namespace ui {
MainApplication* mainApp;
void MainApplication::OnLoad() {
mainApp = this;
this->users_layout = UsersLayout::New();
this->titles_layout = TitlesLayout::New();
this->users_layout->SetOnInput(std::bind(&UsersLayout::onInput, this->users_layout,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->LoadLayout(this->users_layout);
}
} // namespace ui
+95
View File
@@ -0,0 +1,95 @@
// Copyright (C) 2024-2026 NXST contributors
#include <cstdio>
#include <cstring>
#include <map>
#include <sys/stat.h>
#include <vector>
#include <nxst/domain/account.hpp>
#include <nxst/infra/fs/handles.hpp>
static std::map<AccountUid, User> s_users;
Result account::init() {
Result res = accountInitialize(AccountServiceType_Application);
if (R_FAILED(res))
return res;
AccountUid uids[8];
s32 count = 0;
accountListAllUsers(uids, 8, &count);
for (s32 i = 0; i < count; ++i) {
username(uids[i]); // populate cache
}
return 0;
}
void account::exit() {
accountExit();
}
std::vector<AccountUid> account::ids() {
std::vector<AccountUid> result;
result.reserve(s_users.size());
for (const auto& pair : s_users) {
result.push_back(pair.second.id);
}
return result;
}
static User fetchUser(AccountUid id) {
User user{id, ""};
nxst::AccountProfileHandle profile;
AccountProfileBase base{};
if (R_SUCCEEDED(accountGetProfile(profile.get(), id))) {
profile.valid = true;
if (R_SUCCEEDED(accountProfileGet(profile.get(), nullptr, &base))) {
user.name = std::string(base.nickname);
}
}
return user;
}
std::string account::username(AccountUid id) {
auto it = s_users.find(id);
if (it == s_users.end()) {
User user = fetchUser(id);
s_users.emplace(id, user);
return user.name;
}
return it->second.name;
}
std::string account::iconPath(AccountUid id) {
char path[128];
snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg", id.uid[0], id.uid[1]);
struct stat st;
if (stat(path, &st) == 0 && st.st_size > 0)
return std::string(path);
mkdir("sdmc:/switch", 0755);
mkdir("sdmc:/switch/NXST", 0755);
mkdir("sdmc:/switch/NXST/cache", 0755);
nxst::AccountProfileHandle profile;
if (R_FAILED(accountGetProfile(profile.get(), id)))
return "";
profile.valid = true;
u32 img_size = 0;
if (R_FAILED(accountProfileGetImageSize(profile.get(), &img_size)) || img_size == 0)
return "";
std::vector<u8> buf(img_size);
u32 out_size = 0;
if (R_FAILED(accountProfileLoadImage(profile.get(), buf.data(), img_size, &out_size)) || out_size == 0)
return "";
nxst::FileHandle f(fopen(path, "wb"));
if (!f)
return "";
fwrite(buf.data(), 1, out_size, f.get());
return std::string(path);
}
+243
View File
@@ -0,0 +1,243 @@
// Copyright (C) 2024-2026 NXST contributors
#include <algorithm>
#include <cstring>
#include <vector>
#include "nxst/domain/account.hpp"
#include <nxst/domain/title.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/directory.hpp>
#include <nxst/infra/sys/logger.hpp>
using sort_t = enum { SortAlpha, SortLastPlayed, SortPlayTime, SortModesCount };
static constexpr const char* kEmptySave = "New...";
static sort_t s_sort_mode = SortAlpha;
static std::unordered_map<AccountUid, std::vector<Title>> titles;
static bool s_titles_loaded = false;
void Title::init(u8 save_data_type, u64 title_id, AccountUid uid, const std::string& name,
const std::string& author) {
m_id = title_id;
m_uid = uid;
m_save_data_type = save_data_type;
m_user_name = account::username(uid);
m_author = author;
m_name = name;
m_safe_name = string_utils::containsInvalidChar(name) ? string_utils::format("0x%016llX", m_id)
: string_utils::removeForbiddenCharacters(name);
m_path = "sdmc:/switch/NXST/saves/" + string_utils::format("0x%016llX", m_id) + " " + m_safe_name;
std::string aname = string_utils::removeAccents(m_name);
m_display_name = {aname, ""};
size_t colon = aname.rfind(':');
if (colon != std::string::npos) {
std::string head = aname.substr(0, colon);
std::string tail = aname.substr(colon + 1);
string_utils::trim(head);
string_utils::trim(tail);
m_display_name = {head, tail};
} else {
size_t open = aname.rfind('(');
size_t close = aname.rfind(')');
if (open != std::string::npos && close != std::string::npos && close > open) {
std::string head = aname.substr(0, open);
std::string paren = aname.substr(open + 1, close - open - 1);
string_utils::trim(head);
string_utils::trim(paren);
m_display_name = {head, paren};
}
}
refreshDirectories();
}
u8 Title::saveDataType() const {
return m_save_data_type;
}
u64 Title::id() const {
return m_id;
}
u64 Title::saveId() const {
return m_save_id;
}
void Title::saveId(u64 id) {
m_save_id = id;
}
AccountUid Title::userId() const {
return m_uid;
}
std::string Title::userName() const {
return m_user_name;
}
std::string Title::author() const {
return m_author;
}
std::string Title::name() const {
return m_name;
}
std::pair<std::string, std::string> Title::displayName() const {
return m_display_name;
}
std::string Title::path() const {
return m_path;
}
std::string Title::fullPath(size_t index) const {
return m_full_save_paths.at(index);
}
std::vector<std::string> Title::saves() const {
return m_saves;
}
u64 Title::playTimeNanoseconds() const {
return m_play_time_ns;
}
void Title::playTimeNanoseconds(u64 ns) {
m_play_time_ns = ns;
}
u32 Title::lastPlayedTimestamp() const {
return m_last_played_ts;
}
void Title::lastPlayedTimestamp(u32 ts) {
m_last_played_ts = ts;
}
std::string Title::playTime() const {
const u64 minutes = m_play_time_ns / 60000000000ULL;
return string_utils::format("%d", minutes / 60) + ":" + string_utils::format("%02d", minutes % 60) +
" hours";
}
void Title::refreshDirectories() {
m_saves.clear();
m_full_save_paths.clear();
Directory savelist(m_path);
if (savelist.good()) {
for (size_t i = 0; i < savelist.size(); ++i) {
if (savelist.folder(i)) {
m_saves.push_back(savelist.entry(i));
m_full_save_paths.push_back(m_path + "/" + savelist.entry(i));
}
}
std::sort(m_saves.rbegin(), m_saves.rend());
std::sort(m_full_save_paths.rbegin(), m_full_save_paths.rend());
m_saves.insert(m_saves.begin(), kEmptySave);
m_full_save_paths.insert(m_full_save_paths.begin(), kEmptySave);
} else {
nxst::log::error("Could not read save directory for title %s", m_name.c_str());
}
}
bool areTitlesLoaded() {
return s_titles_loaded;
}
void loadTitles() {
if (s_titles_loaded)
return;
s_titles_loaded = true;
titles.clear();
FsSaveDataInfoReader reader;
Result res = fsOpenSaveDataInfoReader(&reader, FsSaveDataSpaceId_User);
if (R_FAILED(res))
return;
std::vector<u8> nacp_buf(sizeof(NsApplicationControlData), 0);
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nacp_buf.data());
FsSaveDataInfo info{};
s64 count = 0;
while (true) {
res = fsSaveDataInfoReaderRead(&reader, &info, 1, &count);
if (R_FAILED(res) || count == 0)
break;
if (info.save_data_type != FsSaveDataType_Account)
continue;
u64 tid = info.application_id;
AccountUid uid = info.uid;
size_t outsize = 0;
NacpLanguageEntry* nle = nullptr;
memset(nsacd, 0, sizeof(NsApplicationControlData));
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd,
sizeof(NsApplicationControlData), &outsize);
if (R_FAILED(res) || outsize < sizeof(nsacd->nacp))
continue;
if (R_FAILED(nacpGetLanguageEntry(&nsacd->nacp, &nle)) || !nle)
continue;
Title title;
title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author));
title.saveId(info.save_data_id);
PdmPlayStatistics stats{};
if (R_SUCCEEDED(pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats))) {
title.playTimeNanoseconds(stats.playtime);
title.lastPlayedTimestamp(stats.last_timestamp_user);
}
auto it = titles.find(uid);
if (it != titles.end()) {
it->second.push_back(title);
} else {
titles.emplace(uid, std::vector<Title>{title});
}
}
fsSaveDataInfoReaderClose(&reader);
sortTitles();
}
void sortTitles() {
for (auto& pair : titles) {
std::sort(pair.second.begin(), pair.second.end(), [](const Title& l, const Title& r) {
switch (s_sort_mode) {
case SortLastPlayed:
return l.lastPlayedTimestamp() > r.lastPlayedTimestamp();
case SortPlayTime:
return l.playTimeNanoseconds() > r.playTimeNanoseconds();
case SortAlpha:
default:
return l.name() < r.name();
}
});
}
}
void rotateSortMode() {
s_sort_mode = static_cast<sort_t>((s_sort_mode + 1) % SortModesCount);
sortTitles();
}
void getTitle(Title& dst, AccountUid uid, size_t i) {
auto it = titles.find(uid);
if (it != titles.end() && i < it->second.size())
dst = it->second[i];
}
size_t getTitleCount(AccountUid uid) {
auto it = titles.find(uid);
return it != titles.end() ? it->second.size() : 0;
}
void refreshDirectories(u64 id) {
for (auto& pair : titles) {
for (auto& title : pair.second) {
if (title.id() == id)
title.refreshDirectories();
}
}
}
std::unordered_map<std::string, std::string> getCompleteTitleList() {
std::unordered_map<std::string, std::string> map;
for (const auto& pair : titles) {
for (const auto& title : pair.second) {
map.emplace(string_utils::format("0x%016llX", title.id()), title.name());
}
}
return map;
}
+250
View File
@@ -0,0 +1,250 @@
// Copyright (C) 2024-2026 NXST contributors
#include <algorithm>
#include <cctype>
#include <cstdarg>
#include <cstdio>
#include <string>
#include <unordered_map>
#include <nxst/domain/account.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/io.hpp>
#include <nxst/infra/sys/logger.hpp>
static bool s_notification_led_available = false;
void servicesExit() {
account::exit();
plExit();
romfsExit();
}
Result servicesInit() {
io::createDirectory("sdmc:/switch");
io::createDirectory("sdmc:/switch/NXST");
io::createDirectory("sdmc:/switch/NXST/saves");
if (appletGetAppletType() != AppletType_Application) {
nxst::log::warn("Please do not run NXST in applet mode.");
}
Result res = 0;
romfsInit();
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
hidInitializeTouchScreen();
if (R_FAILED(res = plInitialize(PlServiceType_User))) {
nxst::log::error("plInitialize failed. Result code 0x%08X.", res);
return res;
}
if (R_FAILED(res = account::init())) {
nxst::log::error("account::init failed. Result code 0x%08X.", res);
return res;
}
if (R_FAILED(res = nsInitialize())) {
nxst::log::error("nsInitialize failed. Result code 0x%08X.", res);
return res;
}
if (R_SUCCEEDED(hidsysInitialize())) {
s_notification_led_available = true;
} else {
nxst::log::info("Notification LED not available.");
}
nxst::log::info("NXST loading completed.");
return 0;
}
bool string_utils::containsInvalidChar(const std::string& str) {
for (unsigned char c : str) {
if (!isascii(c))
return true;
}
return false;
}
std::string string_utils::format(const char* fmt, ...) {
va_list a1, a2;
va_start(a1, fmt);
va_copy(a2, a1);
int n = vsnprintf(nullptr, 0, fmt, a1);
va_end(a1);
if (n < 0) {
va_end(a2);
return {};
}
std::string buf(static_cast<size_t>(n), '\0');
vsnprintf(buf.data(), static_cast<size_t>(n) + 1, fmt, a2);
va_end(a2);
return buf;
}
std::string string_utils::removeForbiddenCharacters(std::string src) {
static constexpr std::string_view kForbidden = ".,!\\/:?*\"<>|";
for (char& c : src) {
if (kForbidden.find(c) != std::string_view::npos)
c = ' ';
}
auto last = src.find_last_not_of(' ');
if (last != std::string::npos)
src.erase(last + 1);
return src;
}
static size_t encodeUtf8(char* out, char32_t cp) {
if (cp < 0x80) {
out[0] = static_cast<char>(cp);
return 1;
}
if (cp < 0x800) {
out[0] = static_cast<char>(0xC0 | (cp >> 6));
out[1] = static_cast<char>(0x80 | (cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
out[0] = static_cast<char>(0xE0 | (cp >> 12));
out[1] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out[2] = static_cast<char>(0x80 | (cp & 0x3F));
return 3;
}
out[0] = static_cast<char>(0xF0 | (cp >> 18));
out[1] = static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
out[2] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
out[3] = static_cast<char>(0x80 | (cp & 0x3F));
return 4;
}
std::string string_utils::UTF16toUTF8(const std::u16string& src) {
std::string result;
result.reserve(src.size() * 2);
for (size_t i = 0; i < src.size(); ++i) {
char32_t cp = src[i];
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < src.size()) {
cp = 0x10000 + ((cp - 0xD800) << 10) + (src[++i] - 0xDC00);
}
char buf[4];
result.append(buf, encodeUtf8(buf, cp));
}
return result;
}
void string_utils::ltrim(std::string& s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char c) {
return !std::isspace(c);
}));
}
void string_utils::rtrim(std::string& s) {
s.erase(std::find_if(s.rbegin(), s.rend(),
[](unsigned char c) {
return !std::isspace(c);
})
.base(),
s.end());
}
void string_utils::trim(std::string& s) {
ltrim(s);
rtrim(s);
}
// Decodes a UTF-8 string to UTF-16, handling surrogate pairs for codepoints > U+FFFF.
std::u16string string_utils::UTF8toUTF16(const char* src) {
std::u16string result;
while (*src != '\0') {
char32_t cp = 0;
unsigned char c = static_cast<unsigned char>(*src);
if (c < 0x80) {
cp = c;
++src;
} else if (c < 0xE0) {
cp = static_cast<char32_t>(c & 0x1F) << 6 | (src[1] & 0x3F);
src += 2;
} else if (c < 0xF0) {
cp = static_cast<char32_t>(c & 0x0F) << 12 | static_cast<char32_t>(src[1] & 0x3F) << 6 |
(src[2] & 0x3F);
src += 3;
} else {
cp = static_cast<char32_t>(c & 0x07) << 18 | static_cast<char32_t>(src[1] & 0x3F) << 12 |
static_cast<char32_t>(src[2] & 0x3F) << 6 | (src[3] & 0x3F);
src += 4;
}
if (cp < 0x10000) {
result += static_cast<char16_t>(cp);
} else {
cp -= 0x10000;
result += static_cast<char16_t>(0xD800 | (cp >> 10));
result += static_cast<char16_t>(0xDC00 | (cp & 0x3FF));
}
}
return result;
}
// Replaces Latin characters with diacritics with their ASCII base equivalents.
std::string string_utils::removeAccents(std::string str) {
static const std::unordered_map<char16_t, char16_t> kMap = {
{u'À', u'A'}, {u'Á', u'A'}, {u'Â', u'A'}, {u'Ã', u'A'}, {u'Ä', u'A'}, {u'Å', u'A'}, {u'Æ', u'E'},
{u'Ç', u'C'}, {u'È', u'E'}, {u'É', u'E'}, {u'Ê', u'E'}, {u'Ë', u'E'}, {u'Ì', u'I'}, {u'Í', u'I'},
{u'Î', u'I'}, {u'Ï', u'I'}, {u'Ð', u'D'}, {u'Ñ', u'N'}, {u'Ò', u'O'}, {u'Ó', u'O'}, {u'Ô', u'O'},
{u'Õ', u'O'}, {u'Ö', u'O'}, {u'Ø', u'O'}, {u'Ù', u'U'}, {u'Ú', u'U'}, {u'Û', u'U'}, {u'Ü', u'U'},
{u'Ý', u'Y'}, {u'ß', u's'}, {u'à', u'a'}, {u'á', u'a'}, {u'â', u'a'}, {u'ã', u'a'}, {u'ä', u'a'},
{u'å', u'a'}, {u'æ', u'e'}, {u'ç', u'c'}, {u'è', u'e'}, {u'é', u'e'}, {u'ê', u'e'}, {u'ë', u'e'},
{u'ì', u'i'}, {u'í', u'i'}, {u'î', u'i'}, {u'ï', u'i'}, {u'ñ', u'n'}, {u'ò', u'o'}, {u'ó', u'o'},
{u'ô', u'o'}, {u'õ', u'o'}, {u'ö', u'o'}, {u'ø', u'o'}, {u'ù', u'u'}, {u'ú', u'u'}, {u'û', u'u'},
{u'ü', u'u'}, {u'ū', u'u'}, {u'ý', u'y'}, {u'ÿ', u'y'},
};
std::u16string wide = UTF8toUTF16(str.c_str());
for (char16_t& ch : wide) {
auto it = kMap.find(ch);
if (it != kMap.end())
ch = it->second;
}
return string_utils::UTF16toUTF8(wide);
}
std::string string_utils::removeNotAscii(std::string str) {
for (char& c : str) {
if (!isascii(static_cast<unsigned char>(c)))
c = ' ';
}
return str;
}
std::string string_utils::elide(const std::string& s, size_t max_chars) {
if (s.size() <= max_chars || max_chars < 6)
return s;
size_t budget = max_chars - 3;
size_t head = (budget + 1) / 2;
size_t tail = budget - head;
return s.substr(0, head) + "..." + s.substr(s.size() - tail);
}
static HidsysNotificationLedPattern makeLedPattern(u8 times) {
HidsysNotificationLedPattern p{};
p.baseMiniCycleDuration = 0x1;
p.totalMiniCycles = 0x2;
p.totalFullCycles = times;
p.startIntensity = 0x0;
p.miniCycles[0] = {0xF, 0xF, 0x0};
p.miniCycles[1] = {0x0, 0xF, 0x0};
return p;
}
void blinkLed(u8 times) {
if (!s_notification_led_available)
return;
PadState pad;
padInitializeDefault(&pad);
s32 n = 0;
HidsysUniquePadId pads[2]{};
HidsysNotificationLedPattern pattern = makeLedPattern(times);
Result res = hidsysGetUniquePadsFromNpad(padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1,
pads, 2, &n);
if (R_SUCCEEDED(res)) {
for (s32 i = 0; i < n; ++i) {
hidsysSetNotificationLedPattern(&pattern, pads[i]);
}
}
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright (C) 2024-2026 NXST contributors
#include <cerrno>
#include <dirent.h>
#include <nxst/infra/fs/directory.hpp>
Directory::Directory(const std::string& path) {
DIR* d = opendir(path.c_str());
if (!d) {
m_error = static_cast<Result>(errno);
return;
}
struct dirent* ent;
while ((ent = readdir(d)) != nullptr) {
m_entries.push_back({ent->d_name, ent->d_type == DT_DIR});
}
closedir(d);
m_good = true;
}
std::string Directory::entry(size_t i) const {
return i < m_entries.size() ? m_entries[i].name : "";
}
bool Directory::folder(size_t i) const {
return i < m_entries.size() && m_entries[i].is_dir;
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright (C) 2024-2026 NXST contributors
#include <nxst/infra/fs/filesystem.hpp>
Result file_system::mount(FsFileSystem* fs, u64 title_id, AccountUid uid) {
return fsOpen_SaveData(fs, title_id, uid);
}
int file_system::mount(FsFileSystem fs) {
return fsdevMountDevice("save", fs);
}
void file_system::unmount() {
fsdevUnmountDevice("save");
}
+319
View File
@@ -0,0 +1,319 @@
// Copyright (C) 2024-2026 NXST contributors
#include <cstdio>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
#include <nxst/domain/account.hpp>
#include <nxst/domain/title.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/directory.hpp>
#include <nxst/infra/fs/filesystem.hpp>
#include <nxst/infra/fs/handles.hpp>
#include <nxst/infra/fs/io.hpp>
#include <nxst/infra/sys/logger.hpp>
static constexpr size_t kBufSize = 0x80000;
bool io::fileExists(const std::string& path) {
struct stat buffer;
return (stat(path.c_str(), &buffer) == 0);
}
void io::copyFile(const std::string& srcPath, const std::string& dstPath) {
nxst::FileHandle src(fopen(srcPath.c_str(), "rb"));
if (!src) {
nxst::log::error("Failed to open source file %s during copy with errno %d. Skipping...",
srcPath.c_str(), errno);
return;
}
nxst::FileHandle dst(fopen(dstPath.c_str(), "wb"));
if (!dst) {
nxst::log::error("Failed to open destination file %s during copy with errno %d. Skipping...",
dstPath.c_str(), errno);
return;
}
fseek(src.get(), 0, SEEK_END);
u64 sz = (u64)ftell(src.get());
rewind(src.get());
std::vector<u8> buf(kBufSize);
u64 offset = 0;
while (offset < sz) {
u32 count = (u32)fread(buf.data(), 1, kBufSize, src.get());
if (count == 0) {
nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.",
srcPath.c_str(), (unsigned long long)offset, (unsigned long long)sz, errno);
break;
}
size_t written = fwrite(buf.data(), 1, count, dst.get());
if (written != count) {
nxst::log::error("fwrite incomplete for %s (%zu/%u bytes). Aborting.", dstPath.c_str(), written,
count);
break;
}
offset += count;
}
if (dstPath.rfind("save:/", 0) == 0) {
fsdevCommitDevice("save");
}
}
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) {
Result res = 0;
bool quit = false;
Directory items(srcPath);
if (!items.good()) {
return items.error();
}
for (size_t i = 0, sz = items.size(); i < sz && !quit; i++) {
std::string newsrc = srcPath + items.entry(i);
std::string newdst = dstPath + items.entry(i);
if (items.folder(i)) {
res = createDirectory(newdst);
if (R_SUCCEEDED(res)) {
newsrc += "/";
newdst += "/";
res = copyDirectory(newsrc, newdst);
} else {
quit = true;
}
} else {
copyFile(newsrc, newdst);
}
}
return res;
}
Result io::createDirectory(const std::string& path) {
if (mkdir(path.c_str(), 0777) != 0 && errno != EEXIST) {
nxst::log::error("mkdir failed for %s (errno %d).", path.c_str(), errno);
return 1;
}
return 0;
}
bool io::directoryExists(const std::string& path) {
struct stat sb;
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
}
Result io::deleteFolderRecursively(const std::string& path) {
Directory dir(path);
if (!dir.good()) {
return dir.error();
}
Result res = 0;
for (size_t i = 0, sz = dir.size(); i < sz; i++) {
if (dir.folder(i)) {
std::string sub = path + "/" + dir.entry(i);
Result sub_res = deleteFolderRecursively(sub + "/");
if (R_FAILED(sub_res))
res = sub_res;
rmdir(sub.c_str());
} else {
std::remove((path + dir.entry(i)).c_str());
}
}
rmdir(path.c_str());
return res;
}
nxst::Result<std::string> io::backup(size_t index, AccountUid uid) {
Title title;
getTitle(title, uid, index);
nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(),
title.id(), title.userId().uid[1], title.userId().uid[0]);
nxst::FsFileSystemHandle fs_handle;
Result res = file_system::mount(fs_handle.get(), title.id(), title.userId());
if (R_FAILED(res)) {
nxst::log::error("Failed to mount filesystem during backup with result 0x%08X. "
"Title id: 0x%016lX; User id: 0x%lX%lX.",
res, title.id(), title.userId().uid[1], title.userId().uid[0]);
return nxst::Result<std::string>::failure("Failed to mount save.");
}
fs_handle.valid = true;
if (file_system::mount(*fs_handle.get()) == -1) {
nxst::log::error("Failed to mount devfs during backup. Title id: 0x%016lX; User id: 0x%lX%lX.",
title.id(), title.userId().uid[1], title.userId().uid[0]);
file_system::unmount();
return nxst::Result<std::string>::failure("Failed to mount save.");
}
fs_handle.release(); // devfs now owns the kernel handle
std::string suggestion =
string_utils::removeNotAscii(string_utils::removeAccents(account::username(title.userId())));
io::createDirectory(title.path());
std::string dst_path = title.path() + "/" + suggestion;
std::string tmp_path = dst_path + ".tmp";
if (io::directoryExists(tmp_path)) {
if (R_FAILED(io::deleteFolderRecursively(tmp_path + "/")))
nxst::log::warn("Failed to clean up stale tmp dir %s.", tmp_path.c_str());
}
res = io::createDirectory(tmp_path);
if (R_FAILED(res)) {
file_system::unmount();
nxst::log::error("Failed to create tmp dir %s.", tmp_path.c_str());
return nxst::Result<std::string>::failure("Failed to create tmp directory.");
}
res = copyDirectory("save:/", tmp_path + "/");
if (R_FAILED(res)) {
file_system::unmount();
io::deleteFolderRecursively(tmp_path + "/");
nxst::log::error("Failed to copy directory to %s with result 0x%08X.", tmp_path.c_str(), res);
return nxst::Result<std::string>::failure("Failed to backup save.");
}
if (io::directoryExists(dst_path)) {
if (R_FAILED(io::deleteFolderRecursively(dst_path + "/")))
nxst::log::warn("Failed to remove old backup at %s.", dst_path.c_str());
}
if (rename(tmp_path.c_str(), dst_path.c_str()) != 0) {
file_system::unmount();
nxst::log::error("Failed to rename temp backup to %s.", dst_path.c_str());
return nxst::Result<std::string>::failure("Failed to finalise backup.");
}
refreshDirectories(title.id());
file_system::unmount();
nxst::log::info("Backup succeeded.");
return nxst::Result<std::string>::success(dst_path);
}
// Creates the save data filesystem for a title if it doesn't exist yet.
static void createSaveIfNeeded(u64 title_id, AccountUid uid) {
std::vector<u8> nsacd_buf(sizeof(NsApplicationControlData), 0);
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data());
size_t outsize = 0;
if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title_id, nsacd,
sizeof(NsApplicationControlData), &outsize))) {
return;
}
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
FsSaveDataAttribute attr = {};
attr.application_id = title_id;
attr.uid = uid;
attr.save_data_type = FsSaveDataType_Account;
attr.save_data_rank = FsSaveDataRank_Primary;
FsSaveDataCreationInfo create_info = {};
create_info.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
create_info.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
create_info.available_size = 0x4000;
create_info.owner_id = nsacd->nacp.save_data_owner_id;
create_info.save_data_space_id = FsSaveDataSpaceId_User;
fsCreateSaveDataFileSystem(&attr, &create_info, &meta);
}
static nxst::Result<void> clearSaveRoot(const std::string& dst_path) {
Directory save_root(dst_path);
for (size_t i = 0, sz = save_root.size(); i < sz; i++) {
if (save_root.folder(i)) {
io::deleteFolderRecursively(dst_path + save_root.entry(i) + "/");
rmdir((dst_path + save_root.entry(i)).c_str());
} else {
std::remove((dst_path + save_root.entry(i)).c_str());
}
}
Result res = fsdevCommitDevice("save");
if (R_FAILED(res)) {
nxst::log::error("Failed to commit save after clearing with result 0x%08X.", res);
return nxst::Result<void>::failure("Failed to commit save after delete.");
}
return nxst::Result<void>::success();
}
static nxst::Result<void> extractAndCommit(const std::string& src_path, const std::string& dst_path) {
Result res = io::copyDirectory(src_path, dst_path);
if (R_FAILED(res)) {
nxst::log::error("Failed to copy %s to save:/ with result 0x%08X.", src_path.c_str(), res);
return nxst::Result<void>::failure("Failed to restore save.");
}
res = fsdevCommitDevice("save");
if (R_FAILED(res)) {
nxst::log::error("Failed to commit save with result 0x%08X.", res);
return nxst::Result<void>::failure("Failed to commit to save device.");
}
return nxst::Result<void>::success();
}
nxst::Result<std::string> io::restore(size_t index, AccountUid uid, const std::string& title_name) {
Title title;
getTitle(title, uid, index);
nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(),
title.id(), title.userId().uid[1], title.userId().uid[0]);
createSaveIfNeeded(title.id(), uid);
nxst::FsFileSystemHandle fs_handle;
Result res = file_system::mount(fs_handle.get(), title.id(), uid);
if (R_FAILED(res)) {
nxst::log::error("Failed to mount filesystem during restore with result 0x%08X. "
"Title id: 0x%016lX; User id: 0x%lX%lX.",
res, title.id(), uid.uid[1], uid.uid[0]);
return nxst::Result<std::string>::failure("Failed to mount save.");
}
fs_handle.valid = true;
if (file_system::mount(*fs_handle.get()) == -1) {
nxst::log::error("Failed to mount devfs during restore. Title id: 0x%016lX; User id: 0x%lX%lX.",
title.id(), uid.uid[1], uid.uid[0]);
file_system::unmount();
return nxst::Result<std::string>::failure("Failed to mount save.");
}
fs_handle.release(); // devfs now owns the kernel handle
std::string suggestion =
string_utils::removeNotAscii(string_utils::removeAccents(account::username(uid)));
std::string src_path = title.path() + "/" + suggestion + "/";
const std::string dst_path = "save:/";
{
Directory src_check(src_path);
if (!src_check.good() || src_check.size() == 0) {
file_system::unmount();
nxst::log::error("Restore source is empty or missing: %s", src_path.c_str());
return nxst::Result<std::string>::failure("Restore source is empty or missing.");
}
}
auto clear_res = clearSaveRoot(dst_path);
if (!clear_res.isOk()) {
file_system::unmount();
return nxst::Result<std::string>::failure(clear_res.error());
}
auto extract_res = extractAndCommit(src_path, dst_path);
if (!extract_res.isOk()) {
file_system::unmount();
return nxst::Result<std::string>::failure(extract_res.error());
}
blinkLed(4);
file_system::unmount();
nxst::log::info("Restore succeeded.");
return nxst::Result<std::string>::success(title_name + "\nhas been restored successfully.");
}
+90
View File
@@ -0,0 +1,90 @@
#include <cstdarg>
#include <cstdio>
#include <ctime>
#include <mutex>
#include <nxst/infra/sys/logger.hpp>
namespace {
std::mutex g_log_mutex;
#if defined(__SWITCH__)
constexpr const char* kLogPath = "/switch/NXST/log.log";
#else
constexpr const char* kLogPath = "nxst.log";
#endif
void writeEntry(const char* tag, const char* fmt, va_list args) {
char msg[2048];
vsnprintf(msg, sizeof(msg), fmt, args);
time_t now = time(nullptr);
struct tm tm_buf;
localtime_r(&now, &tm_buf);
char time_str[16];
strftime(time_str, sizeof(time_str), "%H:%M:%S", &tm_buf);
std::lock_guard<std::mutex> lock(g_log_mutex);
fprintf(stderr, "[%s]%s %s\n", time_str, tag, msg);
FILE* log_file = fopen(kLogPath, "a");
if (log_file != nullptr) {
fprintf(log_file, "[%s]%s %s\n", time_str, tag, msg);
fclose(log_file);
}
}
} // namespace
namespace nxst::log {
void write(Level level, const char* fmt, ...) {
const char* tag = "[INFO] ";
switch (level) {
case Level::Debug:
tag = "[DEBUG]";
break;
case Level::Info:
tag = "[INFO] ";
break;
case Level::Warn:
tag = "[WARN] ";
break;
case Level::Error:
tag = "[ERROR]";
break;
}
va_list args;
va_start(args, fmt);
writeEntry(tag, fmt, args);
va_end(args);
}
void debug(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
writeEntry("[DEBUG]", fmt, args);
va_end(args);
}
void info(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
writeEntry("[INFO] ", fmt, args);
va_end(args);
}
void warn(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
writeEntry("[WARN] ", fmt, args);
va_end(args);
}
void error(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
writeEntry("[ERROR]", fmt, args);
va_end(args);
}
} // namespace nxst::log
+540
View File
@@ -0,0 +1,540 @@
#include <arpa/inet.h>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
#include <nxst/service/transfer_service.hpp>
#ifdef __SWITCH__
#include <switch.h>
#include <nxst/domain/account.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/io.hpp>
#endif
#include <nxst/domain/protocol.hpp>
#include <nxst/infra/net/socket.hpp>
namespace fs = std::filesystem;
namespace nxst {
// ─── File-transfer helpers ────────────────────────────────────────────────────
static bool sendAll(int sock, const void* buf, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
if (n <= 0)
return false;
sent += n;
}
return true;
}
static bool recvAll(int sock, void* buf, size_t len) {
size_t got = 0;
while (got < len) {
ssize_t n = read(sock, static_cast<char*>(buf) + got, len - got);
if (n <= 0)
return false;
got += n;
}
return true;
}
static bool sendFile(int sock, const fs::path& filepath, TransferState& state) {
std::ifstream infile(filepath, std::ios::binary | std::ios::ate);
if (!infile.is_open())
return false;
uint32_t filename_len = (uint32_t)filepath.string().size();
uint64_t file_size = (uint64_t)infile.tellg();
infile.seekg(0, std::ios::beg);
if (!sendAll(sock, &filename_len, sizeof(filename_len)))
return false;
if (!sendAll(sock, filepath.c_str(), filename_len))
return false;
if (!sendAll(sock, &file_size, sizeof(file_size)))
return false;
std::vector<char> buffer(proto::kBufSize);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::kBufSize);
infile.read(buffer.data(), (std::streamsize)to_read);
std::streamsize count = infile.gcount();
if (count <= 0)
break;
if (!sendAll(sock, buffer.data(), (size_t)count))
return false;
state.bytes_done.fetch_add((uint64_t)count);
remaining -= (uint64_t)count;
}
return true;
}
static void mkdirs(const std::string& path) {
for (size_t i = 1; i < path.size(); i++) {
if (path[i] == '/') {
std::string component = path.substr(0, i);
mkdir(component.c_str(), 0777);
}
}
mkdir(path.c_str(), 0777);
}
static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size, TransferState& state) {
size_t last_slash = rel_path.rfind('/');
if (last_slash != std::string::npos) {
std::string dir = rel_path.substr(0, last_slash);
if (!dir.empty())
mkdirs(dir);
}
FILE* outfile = fopen(rel_path.c_str(), "wb");
if (!outfile) {
std::vector<char> drain(proto::kBufSize);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::kBufSize);
ssize_t n = read(sock, drain.data(), to_read);
if (n <= 0)
break;
remaining -= (uint64_t)n;
}
return;
}
state.bytes_total.store(file_size);
state.bytes_done.store(0);
std::vector<char> buffer(proto::kBufSize);
uint64_t total = 0;
while (total < file_size) {
size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::kBufSize);
ssize_t n = read(sock, buffer.data(), to_read);
if (n <= 0)
break;
fwrite(buffer.data(), 1, (size_t)n, outfile);
total += (uint64_t)n;
state.bytes_done.store(total);
}
fclose(outfile);
}
// ─── Sender ──────────────────────────────────────────────────────────────────
void TransferService::failSend(const std::string& reason) {
sender_state.fail_reason = reason;
sender_state.connection_failed.store(true);
sender_state.done.store(true);
}
int TransferService::findServer(char* out_ip) {
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_fd < 0)
return -1;
sender_udp_sock.store(udp_fd);
auto releaseUdp = [&]() {
int owned = sender_udp_sock.exchange(-1);
if (owned == udp_fd)
close(udp_fd);
};
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(proto::kMulticastPort);
addr.sin_addr.s_addr = inet_addr(proto::kMulticastGroup);
if (sendto(udp_fd, "DISCOVER_SERVER", 15, 0, (sockaddr*)&addr, sizeof(addr)) < 0) {
releaseUdp();
return -1;
}
// Poll in 100ms slices so cancel races within 100ms
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < deadline) {
if (sender_state.cancelled.load()) {
releaseUdp();
return -1;
}
struct timeval tv{0, 100000};
fd_set fds;
FD_ZERO(&fds);
FD_SET(udp_fd, &fds);
if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) {
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
char buf[256];
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n > 0) {
buf[n] = '\0';
if (strcmp(buf, "SERVER_HERE") == 0) {
inet_ntop(AF_INET, &from.sin_addr, out_ip, INET_ADDRSTRLEN);
releaseUdp();
return 0;
}
}
}
}
releaseUdp();
return -1;
}
void* TransferService::senderEntry(void* arg) {
auto* a = static_cast<SenderArgs*>(arg);
TransferService* svc = a->svc;
size_t idx = a->title_index;
AccountUid uid = a->uid;
delete a;
svc->runSender(idx, uid);
return nullptr;
}
void TransferService::runSender(size_t title_index, AccountUid uid) {
sender_active.store(true);
auto finish = [this]() {
sender_state.done.store(true);
sender_active.store(false);
};
char server_ip[INET_ADDRSTRLEN];
if (findServer(server_ip) != 0) {
if (!sender_state.cancelled.load())
failSend("No receiver found.\nMake sure the other Switch is in Receive mode.");
return finish();
}
if (sender_state.cancelled.load())
return finish();
sender_state.setStatus("Creating backup...");
#ifdef __SWITCH__
auto backup_result = io::backup(title_index, uid);
if (!backup_result.isOk()) {
failSend("Failed to create backup:\n" + backup_result.error());
return finish();
}
fs::path directory = backup_result.value();
#else
fs::path directory = ".";
(void)title_index;
(void)uid;
#endif
if (sender_state.cancelled.load())
return finish();
sender_state.setStatus("Connecting...");
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_fd < 0) {
failSend("Failed to open socket.");
return finish();
}
sender_tcp_sock.store(tcp_fd);
auto releaseTcp = [&]() {
int owned = sender_tcp_sock.exchange(-1);
if (owned == tcp_fd)
close(tcp_fd);
};
sockaddr_in serv{};
serv.sin_family = AF_INET;
serv.sin_port = htons(proto::kTcpPort);
if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 ||
connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) {
if (!sender_state.cancelled.load())
failSend("Failed to connect to receiver.");
releaseTcp();
return finish();
}
uint64_t total = 0;
for (const auto& entry : fs::recursive_directory_iterator(directory))
if (fs::is_regular_file(entry.path()))
total += fs::file_size(entry.path());
sender_state.bytes_total.store(total);
for (const auto& entry : fs::recursive_directory_iterator(directory)) {
if (sender_state.cancelled.load())
break;
const fs::path& p = entry.path();
if (fs::is_regular_file(p)) {
sender_state.setStatus(p.filename().string());
if (!sendFile(tcp_fd, p, sender_state))
break;
}
}
uint32_t sentinel = proto::kEofSentinel;
sendAll(tcp_fd, &sentinel, sizeof(sentinel));
releaseTcp();
sender_state.setStatus("");
return finish();
}
int TransferService::startSend(size_t title_index, AccountUid uid) {
sender_state.reset();
sender_state.setStatus("Searching for receiver...");
auto* arg = new SenderArgs{this, title_index, uid};
pthread_t thread;
if (pthread_create(&thread, nullptr, senderEntry, arg) != 0) {
delete arg;
return -1;
}
pthread_detach(thread);
return 0;
}
void TransferService::cancelSend() {
sender_state.cancelled.store(true);
int udp = sender_udp_sock.exchange(-1);
if (udp >= 0) {
shutdown(udp, SHUT_RDWR);
close(udp);
}
int tcp = sender_tcp_sock.exchange(-1);
if (tcp >= 0) {
shutdown(tcp, SHUT_RDWR);
close(tcp);
}
}
// ─── Receiver ────────────────────────────────────────────────────────────────
std::string TransferService::replaceUsername(const std::string& file_path) const {
#ifdef __SWITCH__
std::string username =
string_utils::removeNotAscii(string_utils::removeAccents(account::username(restore_uid)));
size_t last_slash = file_path.rfind('/');
if (last_slash == std::string::npos)
return file_path;
size_t prev_slash = file_path.rfind('/', last_slash - 1);
if (prev_slash == std::string::npos)
return username + file_path.substr(last_slash);
return file_path.substr(0, prev_slash + 1) + username + file_path.substr(last_slash);
#else
return file_path;
#endif
}
void* TransferService::broadcastEntry(void* arg) {
static_cast<TransferService*>(arg)->runBroadcast();
return nullptr;
}
void TransferService::runBroadcast() {
receiver_broadcast_active.store(true);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
int udp = socket(AF_INET, SOCK_DGRAM, 0);
if (udp < 0) {
receiver_broadcast_active.store(false);
return;
}
receiver_bcast_sock.store(udp);
auto releaseUdp = [&]() {
int owned = receiver_bcast_sock.exchange(-1);
if (owned == udp)
close(udp);
};
struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit
setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(proto::kMulticastPort);
if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) {
releaseUdp();
receiver_broadcast_active.store(false);
return;
}
ip_mreq group{};
group.imr_multiaddr.s_addr = inet_addr(proto::kMulticastGroup);
group.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) {
releaseUdp();
receiver_broadcast_active.store(false);
return;
}
char buf[256];
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
while (true) {
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n < 0) {
if (receiver_state.cancelled.load())
break;
continue;
}
buf[n] = '\0';
if (strcmp(buf, "DISCOVER_SERVER") == 0) {
sendto(udp, "SERVER_HERE", 11, 0, (sockaddr*)&from, fromlen);
break;
}
}
releaseUdp();
receiver_broadcast_active.store(false);
}
void* TransferService::acceptEntry(void* arg) {
auto* a = static_cast<AcceptArgs*>(arg);
TransferService* svc = a->svc;
int server_fd = a->server_fd;
delete a;
svc->runAccept(server_fd);
return nullptr;
}
void TransferService::runAccept(int server_fd) {
receiver_accept_active.store(true);
receiver_listen_sock.store(server_fd);
sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len);
int owned_listen = receiver_listen_sock.exchange(-1);
if (owned_listen == server_fd)
close(server_fd);
if (client_sock >= 0) {
receiver_client_sock.store(client_sock);
while (true) {
uint32_t filename_len = 0;
if (!recvAll(client_sock, &filename_len, sizeof(filename_len)))
break;
if (filename_len == proto::kEofSentinel)
break;
if (filename_len > proto::kMaxFilename)
break;
std::vector<char> filename_buf(filename_len + 1, '\0');
if (!recvAll(client_sock, filename_buf.data(), filename_len))
break;
std::string filename_str(filename_buf.data(), filename_len);
filename_str = replaceUsername(filename_str);
{
size_t sl = filename_str.rfind('/');
receiver_state.setStatus(sl != std::string::npos ? filename_str.substr(sl + 1)
: filename_str);
}
uint64_t file_size = 0;
if (!recvAll(client_sock, &file_size, sizeof(file_size)))
break;
receiveFile(client_sock, filename_str, file_size, receiver_state);
}
int owned = receiver_client_sock.exchange(-1);
if (owned == client_sock)
close(client_sock);
if (!receiver_state.cancelled.load()) {
#ifdef __SWITCH__
receiver_state.setStatus("Restoring...");
auto result = io::restore(restore_title_index, restore_uid, restore_title_name);
restore_ok = result.isOk();
restore_error = result.isOk() ? "" : result.error();
#else
restore_ok = true;
#endif
}
}
receiver_state.done.store(true);
receiver_accept_active.store(false);
}
int TransferService::startReceive(size_t title_index, AccountUid uid, std::string title_name) {
receiver_state.reset();
receiver_state.setStatus("Waiting for connection...");
restore_title_index = title_index;
restore_uid = uid;
restore_title_name = std::move(title_name);
restore_ok = false;
restore_error.clear();
pthread_t bcast_thread;
if (pthread_create(&bcast_thread, nullptr, broadcastEntry, this) != 0)
return 1;
receiver_bcast_thread = bcast_thread;
pthread_detach(bcast_thread);
Socket server(socket(AF_INET, SOCK_STREAM, 0));
if (!server.valid()) {
cancelReceive();
return 1;
}
int yes = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(proto::kTcpPort);
if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 || listen(server, 3) < 0) {
cancelReceive();
return 1;
}
auto* acc_args = new AcceptArgs{this, server.fd};
pthread_t accept_thread;
if (pthread_create(&accept_thread, nullptr, acceptEntry, acc_args) != 0) {
delete acc_args;
cancelReceive();
return 1;
}
pthread_detach(accept_thread);
server.release();
return 0;
}
void TransferService::cancelReceive() {
receiver_state.cancelled.store(true);
int sock = receiver_client_sock.exchange(-1);
if (sock >= 0) {
shutdown(sock, SHUT_RDWR);
close(sock);
}
int lsock = receiver_listen_sock.exchange(-1);
if (lsock >= 0) {
shutdown(lsock, SHUT_RDWR);
close(lsock);
}
int bsock = receiver_bcast_sock.exchange(-1);
if (bsock >= 0) {
shutdown(bsock, SHUT_RDWR);
close(bsock);
}
if (receiver_broadcast_active.load())
pthread_cancel(receiver_bcast_thread);
}
} // namespace nxst
@@ -1,13 +1,9 @@
#include <MainApplication.hpp> #include <nxst/app/main_application.hpp>
#include <stdio.h> #include <nxst/domain/util.hpp>
#include <main.hpp> #include <nxst/ui/transfer_overlay.hpp>
#include <const.h>
#include <client.hpp>
#include <server.hpp>
#include <TransferOverlay.hpp>
namespace ui { namespace ui {
extern MainApplication *mainApp; extern MainApplication* mainApp;
namespace { namespace {
constexpr int ListX = theme::space::lg; constexpr int ListX = theme::space::lg;
@@ -18,75 +14,70 @@ namespace ui {
constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md; constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md;
constexpr int BtnH = 56; constexpr int BtnH = 56;
constexpr int BtnW = PanelW - 2 * theme::space::lg; constexpr int BtnW = PanelW - 2 * theme::space::lg;
} } // namespace
TitlesLayout::TitlesLayout() : Layout::Layout() { TitlesLayout::TitlesLayout() {
using namespace theme; using namespace theme;
this->titlesMenu = pu::ui::elm::Menu::New( this->titlesMenu =
ListX, ContentY, ListW, pu::ui::elm::Menu::New(ListX, ContentY, ListW, color::BgBase, color::BgSurface2, 88, 6);
color::BgBase, color::BgSurface2,
88, 6);
this->titlesMenu->SetScrollbarColor(color::Primary); this->titlesMenu->SetScrollbarColor(color::Primary);
this->titlesMenu->SetItemsFocusColor(color::BgSurface2); this->titlesMenu->SetItemsFocusColor(color::BgSurface2);
this->titlesMenu->SetOnSelectionChanged([this]() { this->refreshPanel(); }); this->titlesMenu->SetOnSelectionChanged([this]() {
this->refreshPanel();
});
this->SetBackgroundColor(color::BgBase); this->SetBackgroundColor(color::BgBase);
this->Add(this->titlesMenu); this->Add(this->titlesMenu);
this->panelBg = pu::ui::elm::Rectangle::New( this->panelBg =
PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg); pu::ui::elm::Rectangle::New(PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg);
this->Add(this->panelBg); this->Add(this->panelBg);
this->panelTitle = pu::ui::elm::TextBlock::New( this->panelTitle = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg, "");
PanelX + space::lg, ContentY + space::lg, "");
this->panelTitle->SetFont(type::font(type::Title)); this->panelTitle->SetFont(type::font(type::Title));
this->panelTitle->SetColor(color::TextPrimary); this->panelTitle->SetColor(color::TextPrimary);
this->Add(this->panelTitle); this->Add(this->panelTitle);
this->panelHint = pu::ui::elm::TextBlock::New( this->panelHint =
PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:"); pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:");
this->panelHint->SetFont(type::font(type::Body)); this->panelHint->SetFont(type::font(type::Body));
this->panelHint->SetColor(color::TextSecondary); this->panelHint->SetColor(color::TextSecondary);
this->Add(this->panelHint); this->Add(this->panelHint);
int btnY = ContentY + 200; int btnY = ContentY + 200;
this->btnTransferBg = pu::ui::elm::Rectangle::New( this->btnTransferBg =
PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md); pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md);
this->Add(this->btnTransferBg); this->Add(this->btnTransferBg);
this->btnTransferText = pu::ui::elm::TextBlock::New( this->btnTransferText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY + 14,
PanelX + space::lg + space::md, btnY + 14, "Transfer to another device"); "Transfer to another device");
this->btnTransferText->SetFont(type::font(type::Body)); this->btnTransferText->SetFont(type::font(type::Body));
this->btnTransferText->SetColor(color::TextSecondary); this->btnTransferText->SetColor(color::TextSecondary);
this->Add(this->btnTransferText); this->Add(this->btnTransferText);
int btnY2 = btnY + BtnH + space::md; int btnY2 = btnY + BtnH + space::md;
this->btnReceiveBg = pu::ui::elm::Rectangle::New( this->btnReceiveBg =
PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md); pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md);
this->Add(this->btnReceiveBg); this->Add(this->btnReceiveBg);
this->btnReceiveText = pu::ui::elm::TextBlock::New( this->btnReceiveText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY2 + 14,
PanelX + space::lg + space::md, btnY2 + 14, "Receive from another device"); "Receive from another device");
this->btnReceiveText->SetFont(type::font(type::Body)); this->btnReceiveText->SetFont(type::font(type::Body));
this->btnReceiveText->SetColor(color::TextSecondary); this->btnReceiveText->SetColor(color::TextSecondary);
this->Add(this->btnReceiveText); this->Add(this->btnReceiveText);
this->panelFooter = pu::ui::elm::TextBlock::New( this->panelFooter = pu::ui::elm::TextBlock::New(
PanelX + space::lg, PanelX + space::lg, ContentY + ContentH - space::lg - 18, "Save data only");
ContentY + ContentH - space::lg - 18,
"Save data only");
this->panelFooter->SetFont(type::font(type::Caption)); this->panelFooter->SetFont(type::font(type::Caption));
this->panelFooter->SetColor(color::TextMuted); this->panelFooter->SetColor(color::TextMuted);
this->Add(this->panelFooter); this->Add(this->panelFooter);
this->emptyText = pu::ui::elm::TextBlock::New( this->emptyText = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40,
ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40,
"No save data on this profile"); "No save data on this profile");
this->emptyText->SetFont(type::font(type::Display)); this->emptyText->SetFont(type::font(type::Display));
this->emptyText->SetColor(color::TextPrimary); this->emptyText->SetColor(color::TextPrimary);
this->emptyText->SetVisible(false); this->emptyText->SetVisible(false);
this->Add(this->emptyText); this->Add(this->emptyText);
this->emptySub = pu::ui::elm::TextBlock::New( this->emptySub = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16,
ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16,
"Play something first, then come back."); "Play something first, then come back.");
this->emptySub->SetFont(type::font(type::Body)); this->emptySub->SetFont(type::font(type::Body));
this->emptySub->SetColor(color::TextMuted); this->emptySub->SetColor(color::TextMuted);
@@ -98,24 +89,24 @@ namespace ui {
this->updateHints(); this->updateHints();
} }
void TitlesLayout::InitTitles() { void TitlesLayout::InitTitles(AccountUid uid) {
using namespace theme; using namespace theme;
Logger::getInstance().log(Logger::INFO, "InitTitles"); this->current_uid = uid;
auto it = this->menuCache.find(g_currentUId); auto it = this->menuCache.find(uid);
std::vector<pu::ui::elm::MenuItem::Ref>* items; std::vector<pu::ui::elm::MenuItem::Ref>* items;
if (it != this->menuCache.end()) { if (it != this->menuCache.end()) {
items = &it->second; items = &it->second;
} else { } else {
std::vector<pu::ui::elm::MenuItem::Ref> built; std::vector<pu::ui::elm::MenuItem::Ref> built;
for (size_t i = 0; i < getTitleCount(g_currentUId); i++) { for (size_t i = 0; i < getTitleCount(uid); i++) {
Title title; Title title;
getTitle(title, g_currentUId, i); getTitle(title, uid, i);
auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str());
titleItem->SetColor(color::TextPrimary); titleItem->SetColor(color::TextPrimary);
built.push_back(titleItem); built.push_back(titleItem);
} }
auto inserted = this->menuCache.emplace(g_currentUId, std::move(built)); auto inserted = this->menuCache.emplace(uid, std::move(built));
items = &inserted.first->second; items = &inserted.first->second;
} }
@@ -144,15 +135,16 @@ namespace ui {
this->refreshButtons(); this->refreshButtons();
this->updateHints(); this->updateHints();
this->header->SetUser(g_currentUId, Account::username(g_currentUId)); this->header->SetUser(uid, account::username(uid));
} }
void TitlesLayout::refreshPanel() { void TitlesLayout::refreshPanel() {
if (this->titlesMenu->GetItems().empty()) return; if (this->titlesMenu->GetItems().empty())
return;
int idx = this->titlesMenu->GetSelectedIndex(); int idx = this->titlesMenu->GetSelectedIndex();
Title title; Title title;
getTitle(title, g_currentUId, idx); getTitle(title, this->current_uid, idx);
this->panelTitle->SetText(StringUtils::elide(title.name(), 24)); this->panelTitle->SetText(string_utils::elide(title.name(), 24));
} }
void TitlesLayout::refreshButtons() { void TitlesLayout::refreshButtons() {
@@ -185,24 +177,25 @@ namespace ui {
} }
void TitlesLayout::runTransfer(int index, Title& title) { void TitlesLayout::runTransfer(int index, Title& title) {
(void)title;
auto ovl = TransferOverlay::New("Transferring save data..."); auto ovl = TransferOverlay::New("Transferring save data...");
this->titlesMenu->SetVisible(false); this->titlesMenu->SetVisible(false);
mainApp->StartOverlay(ovl); mainApp->StartOverlay(ovl);
this->LockInput(); this->LockInput();
if (transfer_files(index, g_currentUId) != 0) { if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) {
mainApp->EndOverlay(); mainApp->EndOverlay();
this->titlesMenu->SetVisible(true); this->titlesMenu->SetVisible(true);
this->UnlockInput(); this->UnlockInput();
mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true);
return; return;
} }
while (!isClientTransferDone()) { while (!mainApp->transfer.isSendDone()) {
ovl->SetStatus(getClientStatusText()); ovl->SetStatus(mainApp->transfer.sendStatusText());
ovl->SetProgressVisible(isClientProgressKnown()); ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown());
ovl->SetProgress(getClientProgress()); ovl->SetProgress(mainApp->transfer.sendProgress());
mainApp->CallForRender(); mainApp->CallForRender();
if (mainApp->GetButtonsDown() & HidNpadButton_B) { if (mainApp->GetButtonsDown() & HidNpadButton_B) {
cancelClientTransfer(); mainApp->transfer.cancelSend();
} }
svcSleepThread(16666666LL); svcSleepThread(16666666LL);
} }
@@ -210,9 +203,9 @@ namespace ui {
this->titlesMenu->SetVisible(true); this->titlesMenu->SetVisible(true);
this->UnlockInput(); this->UnlockInput();
if (isClientConnectionFailed()) { if (mainApp->transfer.isSendConnectionFailed()) {
mainApp->CreateShowDialog("Transfer", getClientFailReason(), {"OK"}, true); mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true);
} else if (isClientTransferCancelled()) { } else if (mainApp->transfer.isSendCancelled()) {
mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true);
} else { } else {
mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true);
@@ -220,20 +213,21 @@ namespace ui {
} }
void TitlesLayout::runReceive(int index, Title& title) { void TitlesLayout::runReceive(int index, Title& title) {
if (startSendingThread() != 0) { if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) {
mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true); mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.",
{"OK"}, true);
return; return;
} }
auto ovl = TransferOverlay::New("Receiving save data..."); auto ovl = TransferOverlay::New("Receiving save data...");
this->titlesMenu->SetVisible(false); this->titlesMenu->SetVisible(false);
mainApp->StartOverlay(ovl); mainApp->StartOverlay(ovl);
this->LockInput(); this->LockInput();
while (!isServerTransferDone()) { while (!mainApp->transfer.isReceiveDone()) {
ovl->SetStatus(getServerStatusText()); ovl->SetStatus(mainApp->transfer.receiveStatusText());
ovl->SetProgress(getServerProgress()); ovl->SetProgress(mainApp->transfer.receiveProgress());
mainApp->CallForRender(); mainApp->CallForRender();
if (mainApp->GetButtonsDown() & HidNpadButton_B) { if (mainApp->GetButtonsDown() & HidNpadButton_B) {
cancelServerTransfer(); mainApp->transfer.cancelReceive();
} }
svcSleepThread(16666666LL); svcSleepThread(16666666LL);
} }
@@ -241,24 +235,27 @@ namespace ui {
this->titlesMenu->SetVisible(true); this->titlesMenu->SetVisible(true);
this->UnlockInput(); this->UnlockInput();
if (isServerTransferCancelled()) { if (mainApp->transfer.isReceiveCancelled()) {
mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true);
return; } else if (mainApp->transfer.restoreSucceeded()) {
} mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"},
auto restoreResult = io::restore(index, g_currentUId, 0, title.name()); true);
if (std::get<0>(restoreResult)) {
mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true);
} else { } else {
mainApp->CreateShowDialog("Receive", "Restore failed:\n" + std::get<2>(restoreResult), {"OK"}, true); mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(),
{"OK"}, true);
} }
} }
void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
if (m_inputLocked) return; (void)Up;
(void)Held;
(void)Pos;
if (m_inputLocked)
return;
if (Down & HidNpadButton_Plus) { if (Down & HidNpadButton_Plus) {
cancelClientTransfer(); mainApp->transfer.cancelSend();
cancelServerTransfer(); mainApp->transfer.cancelReceive();
mainApp->Close(); mainApp->Close();
return; return;
} }
@@ -266,11 +263,12 @@ namespace ui {
if (focus == TitlesFocus::List) { if (focus == TitlesFocus::List) {
if (Down & HidNpadButton_B) { if (Down & HidNpadButton_B) {
this->header->SetUser(std::nullopt, ""); this->header->SetUser(std::nullopt, "");
mainApp->LoadLayout(mainApp->usersLayout); mainApp->LoadLayout(mainApp->users_layout);
return; return;
} }
if (Down & HidNpadButton_A) { if (Down & HidNpadButton_A) {
if (this->titlesMenu->GetItems().empty()) return; if (this->titlesMenu->GetItems().empty())
return;
this->lockedListIndex = this->titlesMenu->GetSelectedIndex(); this->lockedListIndex = this->titlesMenu->GetSelectedIndex();
this->focus = TitlesFocus::Actions; this->focus = TitlesFocus::Actions;
this->action = TitlesAction::Transfer; this->action = TitlesAction::Transfer;
@@ -288,17 +286,17 @@ namespace ui {
this->updateHints(); this->updateHints();
return; return;
} }
if (Down & (HidNpadButton_Up | HidNpadButton_Down | if (Down &
HidNpadButton_StickLUp | HidNpadButton_StickLDown)) { (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_StickLUp | HidNpadButton_StickLDown)) {
this->action = (action == TitlesAction::Transfer) this->action =
? TitlesAction::Receive : TitlesAction::Transfer; (action == TitlesAction::Transfer) ? TitlesAction::Receive : TitlesAction::Transfer;
this->refreshButtons(); this->refreshButtons();
return; return;
} }
if (Down & HidNpadButton_A) { if (Down & HidNpadButton_A) {
int idx = this->titlesMenu->GetSelectedIndex(); int idx = this->titlesMenu->GetSelectedIndex();
Title title; Title title;
getTitle(title, g_currentUId, idx); getTitle(title, this->current_uid, idx);
TitlesAction chosen = action; TitlesAction chosen = action;
this->focus = TitlesFocus::List; this->focus = TitlesFocus::List;
this->refreshButtons(); this->refreshButtons();
@@ -311,4 +309,4 @@ namespace ui {
} }
} }
} }
} } // namespace ui
@@ -1,34 +1,26 @@
#include <cstdio> #include <nxst/app/main_application.hpp>
#include <MainApplication.hpp>
#include "main.hpp"
namespace ui { namespace ui {
extern MainApplication *mainApp; extern MainApplication* mainApp;
UsersLayout::UsersLayout() : Layout::Layout() { UsersLayout::UsersLayout() : Layout::Layout() {
using namespace theme; using namespace theme;
this->usersMenu = pu::ui::elm::Menu::New( this->usersMenu = pu::ui::elm::Menu::New(0, layout::ContentTop + space::md, layout::ScreenW,
0, layout::ContentTop + space::md, color::BgBase, color::BgSurface2, 88, 6);
layout::ScreenW,
color::BgBase, color::BgSurface2,
88, 6);
this->usersMenu->SetScrollbarColor(color::Primary); this->usersMenu->SetScrollbarColor(color::Primary);
this->usersMenu->SetItemsFocusColor(color::BgSurface2); this->usersMenu->SetItemsFocusColor(color::BgSurface2);
for (AccountUid const& uid : Account::ids()) { for (AccountUid const& uid : account::ids()) {
auto item = pu::ui::elm::MenuItem::New(Account::username(uid)); auto item = pu::ui::elm::MenuItem::New(account::username(uid));
item->SetColor(color::TextPrimary); item->SetColor(color::TextPrimary);
this->usersMenu->AddItem(item); this->usersMenu->AddItem(item);
} }
this->loadingBg = pu::ui::elm::Rectangle::New( this->loadingBg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
this->loadingBg->SetVisible(false); this->loadingBg->SetVisible(false);
this->loadingText = pu::ui::elm::TextBlock::New( this->loadingText = pu::ui::elm::TextBlock::New(layout::ScreenW / 2 - 120, layout::ScreenH / 2 - 12,
layout::ScreenW / 2 - 120,
layout::ScreenH / 2 - 12,
"Loading saves..."); "Loading saves...");
this->loadingText->SetFont(type::font(type::Body)); this->loadingText->SetFont(type::font(type::Body));
this->loadingText->SetColor(color::TextSecondary); this->loadingText->SetColor(color::TextSecondary);
@@ -50,11 +42,12 @@ namespace ui {
void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
if (Down & HidNpadButton_Plus) { if (Down & HidNpadButton_Plus) {
svcExitProcess(); mainApp->Close();
return;
} }
if (Down & HidNpadButton_A) { if (Down & HidNpadButton_A) {
g_currentUId = Account::ids().at(this->usersMenu->GetSelectedIndex()); AccountUid uid = account::ids().at(this->usersMenu->GetSelectedIndex());
if (!areTitlesLoaded()) { if (!areTitlesLoaded()) {
this->usersMenu->SetVisible(false); this->usersMenu->SetVisible(false);
@@ -69,8 +62,8 @@ namespace ui {
this->usersMenu->SetVisible(true); this->usersMenu->SetVisible(true);
} }
mainApp->titlesLayout->InitTitles(); mainApp->titles_layout->InitTitles(uid);
mainApp->LoadLayout(mainApp->titlesLayout); mainApp->LoadLayout(mainApp->titles_layout);
} }
} }
} } // namespace ui
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
find src include -name '*.cpp' -o -name '*.hpp' | xargs clang-format -i
echo "Formatted."