diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..daced9b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. diff --git a/Makefile b/Makefile index b2a96f1..730fabb 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ INCLUDES := include include/fs lib/Plutonium/include EXEFS_SRC := exefs_src APP_TITLE := NXST APP_AUTHOR := DragonSpirit -APP_VERSION := 01.28.2024 +APP_VERSION := 04.24.2026 ICON := icon.jpg diff --git a/source/TitlesLayout.cpp b/source/TitlesLayout.cpp index 613d28b..26a2ed7 100644 --- a/source/TitlesLayout.cpp +++ b/source/TitlesLayout.cpp @@ -63,7 +63,7 @@ namespace ui { printf("Failed to restore with error %s\n", std::get<2>(restoreResult).c_str()); } } - + break; } } diff --git a/source/account.cpp b/source/account.cpp index 37dc212..1bca46f 100644 --- a/source/account.cpp +++ b/source/account.cpp @@ -56,7 +56,7 @@ static User getUser(AccountUid id) memset(&profilebase, 0, sizeof(profilebase)); if (R_SUCCEEDED(accountGetProfile(&profile, id)) && R_SUCCEEDED(accountProfileGet(&profile, NULL, &profilebase))) { - user.name = std::string(profilebase.nickname, 0x20); + user.name = std::string(profilebase.nickname); } accountProfileClose(&profile); diff --git a/source/client.cpp b/source/client.cpp index de99eb7..9b5b8ba 100644 --- a/source/client.cpp +++ b/source/client.cpp @@ -17,7 +17,7 @@ #include #endif #define PORT 8080 -#define BUFFER_SIZE 1024 +#define BUFFER_SIZE 65536 #define MULTICAST_PORT 8081 #define MULTICAST_GROUP "239.0.0.1" // Multicast group IP @@ -37,11 +37,14 @@ struct ThreadArgs { fs::path directory; }; -bool receiveAck(int sock) { - char ack[4] = {0}; - int bytes_received = read(sock, ack, 3); - std::cout << "receiveAck bytes_received: " << bytes_received << std::endl; - return bytes_received > 0 && std::string(ack) == "ACK"; +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(buf) + sent, len - sent, 0); + if (n <= 0) return false; + sent += n; + } + return true; } // Отправка строки с учетом её длины @@ -76,7 +79,7 @@ void sendFile(int sock, const fs::path &base_path, const fs::path &filepath) { // Get the size of the file std::streamsize file_size = infile.tellg(); infile.seekg(0, std::ios::beg); - char buffer[BUFFER_SIZE]; + char* buffer = new char[BUFFER_SIZE]; std::cout << "send filesize: " << file_size << std::endl; @@ -87,22 +90,18 @@ void sendFile(int sock, const fs::path &base_path, const fs::path &filepath) { while (file_size > 0) { infile.read(buffer, BUFFER_SIZE); - ssize_t bytes_sent = send(sock, buffer, infile.gcount(), 0); - if (bytes_sent == -1) { + std::streamsize count = infile.gcount(); + if (!send_all(sock, buffer, count)) { std::cerr << "Failed to send file data" << std::endl; + delete[] buffer; pthread_exit(nullptr); } - file_size -= bytes_sent; - - // Wait for acknowledgment after each chunk - if (!receiveAck(sock)) { - std::cerr << "Failed to receive acknowledgment" << std::endl; - pthread_exit(nullptr); - } + file_size -= count; } std::cout << "File sent successfully: " << filepath << std::endl; + delete[] buffer; infile.close(); } diff --git a/source/io.cpp b/source/io.cpp index 360b78f..068c288 100644 --- a/source/io.cpp +++ b/source/io.cpp @@ -45,7 +45,7 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath) } 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 %d. Skipping...", errno); + Logger::getInstance().log(Logger::ERROR, "Failed to open destination file " + dstPath + " during copy with errno " + std::to_string(errno) + ". Skipping..."); fclose(src); return; } @@ -70,9 +70,7 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath) fclose(src); fclose(dst); - // commit each file to the save if (dstPath.rfind("save:/", 0) == 0) { - Logger::getInstance().log(Logger::ERROR, "Committing file " + dstPath + " to the save archive."); fsdevCommitDevice("save"); } @@ -217,32 +215,74 @@ std::tuple io::restore(size_t index, AccountUid uid, 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(), title.userId()); + res = FileSystem::mount(&fileSystem, title.id(), uid); if (R_SUCCEEDED(res)) { int rc = FileSystem::mount(fileSystem); if (rc == -1) { FileSystem::unmount(); Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(), - title.userId().uid[1], title.userId().uid[0]); + 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(), - title.userId().uid[1], title.userId().uid[0]); + uid.uid[1], uid.uid[0]); return std::make_tuple(false, res, "Failed to mount save."); } - std::string srcPath = title.fullPath(cellIndex) + "/"; + std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid))); + std::string srcPath = title.path() + "/" + suggestion + "/"; std::string dstPath = "save:/"; - res = io::deleteFolderRecursively(dstPath.c_str()); + { + 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 recursively delete directory " + dstPath + " with result 0x%08lX.", res); - return std::make_tuple(false, res, "Failed to delete save."); + 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); diff --git a/source/server.cpp b/source/server.cpp index 5873062..66a3110 100644 --- a/source/server.cpp +++ b/source/server.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include @@ -19,7 +21,7 @@ #endif #define PORT 8080 -#define BUFFER_SIZE 1024 +#define BUFFER_SIZE 65536 #define MULTICAST_PORT 8081 #define MULTICAST_GROUP "239.0.0.1" // Multicast group IP @@ -27,7 +29,8 @@ namespace fs = std::filesystem; #ifdef __SWITCH__ std::string replaceUsername(const std::string &path) { - std::string replacedString = Account::username(g_currentUId); + std::string replacedString = StringUtils::removeNotAscii( + StringUtils::removeAccents(Account::username(g_currentUId))); // Найдём позицию последнего символа '/' size_t lastSlashPos = path.rfind('/'); @@ -51,42 +54,91 @@ std::string replaceUsername(const std::string &path) { } #endif -void sendAck(int sock) { - const char *ack = "ACK"; - std::cout << "Sending ACK " << std::endl; - send(sock, ack, strlen(ack), 0); +// Читает ровно len байт из сокета, повторяя read при частичном получении. +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(buf) + received, len - received); + if (n <= 0) return false; + received += n; + } + return true; +} + +// Создаёт все компоненты пути через POSIX mkdir. +// std::filesystem::create_directories не работает с devkitPro-путями sdmc:/. +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); + int rc = mkdir(component.c_str(), 0777); + std::cout << "mkdirs: mkdir [" << component << "] rc=" << rc << " errno=" << errno << std::endl; + } + } + int rc = mkdir(path.c_str(), 0777); + std::cout << "mkdirs: mkdir [" << path << "] rc=" << rc << " errno=" << errno << std::endl; } // Функция для получения файла void receive_file(int sock, const std::string &relative_path, size_t file_size) { - fs::path filepath(relative_path); - // Create parent directories if they do not exist - std::cout << "relative_path is: " << relative_path << std::endl; - fs::create_directories(filepath.parent_path()); + // Печатаем путь побайтово — ловим невидимые символы + std::cout << "receive_file len=" << relative_path.size() << " path=["; + for (unsigned char c : relative_path) { + if (c >= 0x20 && c <= 0x7e) std::cout << c; + else std::cout << "\\x" << std::hex << std::setw(2) << std::setfill('0') << (int)c << std::dec; + } + std::cout << "]" << std::endl; - std::ofstream outfile(filepath, std::ios::binary); - char buffer[BUFFER_SIZE] = {0}; + size_t last_slash = relative_path.rfind('/'); + std::string dir = (last_slash != std::string::npos) + ? relative_path.substr(0, last_slash) + : ""; + std::cout << "receive_file dir=[" << dir << "]" << std::endl; + if (!dir.empty()) mkdirs(dir); + // Проверяем stat на папке перед fopen + struct stat st; + int statrc = stat(dir.c_str(), &st); + std::cout << "stat(dir) rc=" << statrc << " is_dir=" + << (statrc == 0 && S_ISDIR(st.st_mode)) << std::endl; + + FILE *outfile = fopen(relative_path.c_str(), "wb"); + if (!outfile) { + int saved_errno = errno; + std::cerr << "Failed to open for writing: " << relative_path + << " dir=[" << dir << "] fopen_errno=" << saved_errno << std::endl; + // Дренируем байты, чтобы отправитель не завис + char* drain_buf = new char[BUFFER_SIZE]; + size_t remaining = file_size; + while (remaining > 0) { + ssize_t n = read(sock, drain_buf, remaining < (size_t)BUFFER_SIZE ? remaining : (size_t)BUFFER_SIZE); + if (n <= 0) break; + remaining -= n; + } + delete[] drain_buf; + return; + } + + char* buffer = new char[BUFFER_SIZE](); size_t total_bytes_received = 0; while (total_bytes_received < file_size) { - ssize_t bytes_received = read(sock, buffer, BUFFER_SIZE); + size_t to_read = std::min((size_t)BUFFER_SIZE, file_size - total_bytes_received); + ssize_t bytes_received = read(sock, buffer, to_read); std::cout << "Bytes received: " << bytes_received << std::endl; if (bytes_received <= 0) { std::cerr << "Error reading file data from socket." << std::endl; break; } - outfile.write(buffer, bytes_received); + fwrite(buffer, 1, bytes_received, outfile); total_bytes_received += bytes_received; - - // Send acknowledgment for each chunk received - sendAck(sock); } std::cout << "File received successfully: " << relative_path << std::endl; - outfile.close(); + delete[] buffer; + fclose(outfile); } void *handle_client(void *socket_desc) { @@ -108,14 +160,16 @@ void *handle_client(void *socket_desc) { pthread_exit(nullptr); } - // Receive filename or directory name - char *filename = new char[filename_len + 1]; - read(client_socket, filename, filename_len); - + // Receive filename + char *filename = new char[filename_len + 1](); + if (!recv_all(client_socket, filename, filename_len)) { + std::cerr << "Short read on filename, aborting." << std::endl; + delete[] filename; + break; + } filename[filename_len] = '\0'; std::string filename_str(filename); - std::cout << "Receive filename: " << filename_str << std::endl; - delete[] filename; // Clean up filename buffer + delete[] filename; std::cout << "Received filename_str is " << filename_str << std::endl; @@ -126,7 +180,10 @@ void *handle_client(void *socket_desc) { #endif size_t file_size; - read(client_socket, &file_size, sizeof(file_size)); + if (!recv_all(client_socket, &file_size, sizeof(file_size))) { + std::cerr << "Short read on file_size, aborting." << std::endl; + break; + } std::cout << "file size is: " << file_size << std::endl; receive_file(client_socket, filename_str, file_size); } @@ -138,7 +195,7 @@ void *handle_client(void *socket_desc) { void *broadcast_listener(void *) { int sockfd; struct sockaddr_in servaddr; - char buffer[BUFFER_SIZE]; + char buffer[BUFFER_SIZE + 1]; struct ip_mreq group; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {