#include "users.hpp" #include #include #include #include #include #include #include // Definition of the global instance. Users users; // ================================================================= // == 1. NANOPB STREAM HELPERS for Arduino File Objects // ================================================================= // A state structure to pass the Arduino File object to Nanopb's stream callbacks. struct FileStreamState { File* file; }; // Nanopb callback for writing data to a File. static bool writeFileCallback(pb_ostream_t *stream, const uint8_t *buf, size_t count) { FileStreamState* state = (FileStreamState*)stream->state; return state->file->write(buf, count) == count; } // Nanopb callback for reading from a File. static bool readFileCallback(pb_istream_t *stream, uint8_t *buf, size_t count) { FileStreamState* state = (FileStreamState*)stream->state; if (count == 0) return true; size_t len = state->file->read(buf, count); if (len < count) { stream->bytes_left = 0; // Signal End-Of-File } return len > 0 || count == 0; } // Helper to create a Nanopb output stream from an Arduino File object. static pb_ostream_t pb_ostream_from_file(File &file) { FileStreamState* state = new FileStreamState{&file}; pb_ostream_t stream = {&writeFileCallback, state, SIZE_MAX, 0}; return stream; } // Helper to create a Nanopb input stream from an Arduino File object. static pb_istream_t pb_istream_from_file(File &file) { FileStreamState* state = new FileStreamState{&file}; pb_istream_t stream = {&readFileCallback, state, file.size()}; return stream; } // This structure is used to pass data to the nanopb decode callbacks // when decoding into a single, stack-allocated UserData object. struct DecodeTarget { UserData* user_buffer; }; // Nanopb callback to decode a username string into our temporary buffer. static bool decodeUsernameCallback(pb_istream_t *stream, const pb_field_t *field, void **arg) { DecodeTarget* target = (DecodeTarget*)*arg; // 1. Store the length of the incoming string BEFORE reading from the stream. size_t len = stream->bytes_left; if (len >= sizeof(target->user_buffer->username)) { return false; // Input string is too long for our buffer. } // 2. Read 'len' bytes. This will modify stream->bytes_left. if (!pb_read(stream, (uint8_t*)target->user_buffer->username, len)) { return false; } // 3. Use the stored length 'len' to place the null terminator correctly. target->user_buffer->username[len] = '\0'; return true; } // Nanopb callback to decode a password hash (bytes) into our temporary buffer. static bool decodePasswordCallback(pb_istream_t *stream, const pb_field_t *field, void **arg) { DecodeTarget* target = (DecodeTarget*)*arg; if (stream->bytes_left != sizeof(target->user_buffer->password_hash)) { return false; // A SHA-256 hash MUST be 32 bytes. } return pb_read(stream, target->user_buffer->password_hash, stream->bytes_left); } // ================================================================= // == 2. USERS CLASS IMPLEMENTATION // ================================================================= Users::Users() {} Users::~Users() {} void Users::hashPassword(const char* password, uint8_t* hash_out) const { SHA256 sha256; sha256.update(password, strlen(password)); sha256.finalize(hash_out, 32); } // --- PRIVATE STATIC HELPERS (Moved inside the class scope) --- // Generic helper function to iterate over all users in the file // and execute a callback for each one. The callback can stop the iteration by returning false. bool Users::_iterateUsers(const char* filename, const std::function& user_callback) { // check if file exists if(!LittleFS.exists(filename)){ return true; // No file is not an error, just means no users to iterate. } File file = LittleFS.open(filename, "r"); if (!file || file.size() == 0) { return true; // No file is not an error, just means no users to iterate. } pb_istream_t istream = pb_istream_from_file(file); UserData user_buffer; // Single user buffer on the stack DecodeTarget target = { &user_buffer }; webui_WebUiUser user_msg = webui_WebUiUser_init_zero; user_msg.password_hash.funcs.decode = &decodePasswordCallback; user_msg.password_hash.arg = ⌖ bool continue_iterating = true; while (istream.bytes_left > 0) { memset(&user_buffer, 0, sizeof(user_buffer)); if (pb_decode_delimited(&istream, webui_WebUiUser_fields, &user_msg)) { strcpy(user_buffer.username, user_msg.username); if (!user_callback(user_buffer)) { continue_iterating = false; break; } } else { // Decoding failed, stop iterating. break; } } delete (FileStreamState*)istream.state; file.close(); return continue_iterating; } // Generic helper for streaming transformations (delete, update). // The callback modifies a user buffer and returns true to keep it, false to delete it. bool Users::_transformUsers(const char* src_filename, const std::function& transform_callback) { const char* tmp_filename = "/users.pb.tmp"; File src_file = LittleFS.open(src_filename, "r"); if (!src_file) return true; File dst_file = LittleFS.open(tmp_filename, "w"); if (!dst_file) { src_file.close(); return false; } pb_istream_t istream = pb_istream_from_file(src_file); pb_ostream_t ostream = pb_ostream_from_file(dst_file); bool success = true; UserData user_buffer; // Our 1-user buffer // --- CORRECTION START --- // 1. A structure ONLY for decoding (reading) DecodeTarget decode_target = { &user_buffer }; webui_WebUiUser decode_msg = webui_WebUiUser_init_zero; decode_msg.password_hash.funcs.decode = &decodePasswordCallback; decode_msg.password_hash.arg = &decode_target; // 2. A structure ONLY for encoding (writing) webui_WebUiUser encode_msg = webui_WebUiUser_init_zero; encode_msg.password_hash.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) { UserData* u = (UserData*)*arg; return pb_encode_tag_for_field(stream, field) && pb_encode_string(stream, u->password_hash, sizeof(u->password_hash)); }; // 3. Main loop: Decode with decode_msg, transform user_buffer, encode with encode_msg while (istream.bytes_left > 0 && pb_decode_delimited(&istream, webui_WebUiUser_fields, &decode_msg)) { strcpy(user_buffer.username, decode_msg.username); if (transform_callback(user_buffer)) { // Keep: Encode the content from the buffer with the encode_msg structure strcpy(encode_msg.username, user_buffer.username); encode_msg.password_hash.arg = &user_buffer; if (!pb_encode_delimited(&ostream, webui_WebUiUser_fields, &encode_msg)) { success = false; break; } } } // --- Rest of the function remains the same --- delete (FileStreamState*)istream.state; delete (FileStreamState*)ostream.state; src_file.close(); dst_file.close(); if (!success) { LittleFS.remove(tmp_filename); return false; } if (LittleFS.exists(src_filename)) { LittleFS.remove(src_filename); } return LittleFS.rename(tmp_filename, src_filename); } // --- PUBLIC READ-ONLY METHODS --- size_t Users::getUserCount() { if(initialized_) { return userCount_; } size_t count = 0; _iterateUsers(filename_, [&](UserData& user) { count++; return true; // Continue iterating }); userCount_ = count; initialized_ = true; return count; } bool Users::userExists(const char* username) const { bool found = false; _iterateUsers(filename_, [&](UserData& user) { if (strcmp(user.username, username) == 0) { found = true; return false; // Stop iterating } return true; // Continue iterating }); return found; } bool Users::checkPassword(const char* username, const char* password) const { bool success = false; _iterateUsers(filename_, [&](UserData& user) { if (strcmp(user.username, username) == 0) { uint8_t provided_hash[32]; hashPassword(password, provided_hash); if (memcmp(user.password_hash, provided_hash, 32) == 0) { success = true; } return false; // Stop iterating, user found. } return true; // Continue iterating }); return success; } bool Users::loadUsernamesCallback(const std::function &callback) const { if (!callback) return false; return _iterateUsers(filename_, [&](UserData &user) { auto res = callback(user.username); if (!res) return false; // Stop iterating return true; // Continue iterating }); } bool Users::addUser(const char* username, const char* password) { // --- Phase 1: Checking preconditions --- bool exists = userExists(username); size_t count = getUserCount(); if (exists || count >= MAX_USERS) { if (exists) { LOG_ERROR("addUser: User already exists. Aborting."); } else { LOG_ERROR("addUser: User limit reached. Aborting."); } return false; } // --- Phase 2: Preparing the files --- const char* tmp_filename = "/users.pb.tmp"; File dst_file = LittleFS.open(tmp_filename, "w"); if (!dst_file) { LOG_ERROR("addUser: Failed to open temporary file. Check filesystem space and permissions."); return false; } File src_file = LittleFS.open(filename_, "r"); if (src_file && src_file.size() > 0) { uint8_t buffer[128]; while (src_file.available()) { size_t len = src_file.read(buffer, sizeof(buffer)); dst_file.write(buffer, len); } src_file.close(); } else { if (src_file) src_file.close(); } // --- Phase 3: Appending the new user --- pb_ostream_t ostream = pb_ostream_from_file(dst_file); webui_WebUiUser new_user_msg = webui_WebUiUser_init_zero; // Prepare username for the encoder strcpy(new_user_msg.username, username); // Prepare password hash for the encoder uint8_t password_hash[32]; hashPassword(password, password_hash); // Important: Hash is created here, not in the lambda new_user_msg.password_hash.arg = password_hash; new_user_msg.password_hash.funcs.encode = [](pb_ostream_t *stream, const pb_field_t *field, void * const *arg) { return pb_encode_tag_for_field(stream, field) && pb_encode_string(stream, (uint8_t*)*arg, 32); }; bool status = pb_encode_delimited(&ostream, webui_WebUiUser_fields, &new_user_msg); // Important: Always release resources (stream state and file) delete (FileStreamState*)ostream.state; dst_file.close(); if (!status) { LOG_ERROR("addUser: Protobuf encoding (pb_encode_delimited) failed!"); LOG_DEBUG("addUser: Removing failed temporary file: %s\n", tmp_filename); LittleFS.remove(tmp_filename); return false; } // --- Phase 4: Atomic replacement of the old file --- if (LittleFS.exists(filename_)) { if (!LittleFS.remove(filename_)) { LOG_ERROR("addUser: Could not remove old file '%s'. Aborting.\n", filename_); LittleFS.remove(tmp_filename); // Cleanup temp file return false; } } bool rename_ok = LittleFS.rename(tmp_filename, filename_); if (rename_ok) { } else { LOG_ERROR("addUser: Final rename failed! Filesystem might be in an inconsistent state."); LittleFS.remove(tmp_filename); // Attempt to clean up } if (rename_ok) { userCount_++; } return rename_ok; } bool Users::deleteUser(const char* username) { bool user_was_found = false; bool success = _transformUsers(filename_, [&](UserData& user) { if (strcmp(user.username, username) == 0) { user_was_found = true; return false; // Delete this user } return true; // Keep this user }); bool user_deleted = success && user_was_found; if (user_deleted) { userCount_--; } return user_deleted; } bool Users::updatePassword(const char* username, const char* newPassword) { bool user_was_found = false; bool success = _transformUsers(filename_, [&](UserData& user) { if (strcmp(user.username, username) == 0) { user_was_found = true; hashPassword(newPassword, user.password_hash); } return true; // Always keep the user, just maybe modified. }); return success && user_was_found; } void Users::reset() { if (LittleFS.exists(filename_)) { LittleFS.remove(filename_); } userCount_ = 0; initialized_ = false; }