diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e0e0551..7147e5b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ## [15.2.0.3] ### Added +- Berry `path.listdir("file.tapp#")` to list directory inside '.tapp' archives ### Breaking Changed diff --git a/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.cpp b/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.cpp index 5d5590f36..1d90cfa22 100644 --- a/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.cpp +++ b/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.cpp @@ -433,6 +433,76 @@ bool ZipArchive::parse(void) { } +/******************************************************************** +** Iterate over all files in a ZIP archive +** This is a lightweight iterator that doesn't store entries in memory +********************************************************************/ +bool ZipArchiveIterator(File &zipfile, ZipIteratorCallback callback, void *user_data) { + ZipHeader header; + zipfile.seek(0); // start of file + int32_t offset = 0; + const size_t zip_header_size = sizeof(header) - sizeof(header.padding); + + while (1) { + zipfile.seek(offset); + int32_t bytes_read = zipfile.read(sizeof(header.padding) + (uint8_t*) &header, zip_header_size); + if (bytes_read != (int32_t)zip_header_size) { + break; + } + + // Check ZIP local file header signature (0x04034B50 split as 0x4B50 and 0x0403) + if (header.signature1 != 0x4B50) { + break; // Invalid signature + } + if (header.signature2 != 0x0403) { + break; // End of local file headers (could be central directory) + } + + // Check for unsupported features + if (header.gen_purpose_flags != 0x0000) { + break; // Unsupported flags + } + if (header.compression != 0x0000) { + break; // Compressed files not supported + } + + // Check file name size + if (header.filename_size > 256 || header.filename_size == 0) { + break; // Filename too long or empty + } + + // Read filename + char fname[header.filename_size + 1]; + if (zipfile.read((uint8_t*)fname, header.filename_size) != header.filename_size) { + break; + } + fname[header.filename_size] = '\0'; + + // Extract just the filename suffix after the last '#' (same logic as ZipArchive::parse) + char *fname_suffix; + char *saveptr; + fname_suffix = strtok_r(fname, "#", &saveptr); + char *res = fname_suffix; + while (res) { + res = strtok_r(nullptr, "#", &saveptr); + if (res) { fname_suffix = res; } + } + + // Call the callback with the filename + if (fname_suffix && fname_suffix[0] != '\0') { + if (!callback(fname_suffix, user_data)) { + break; // Callback requested to stop iteration + } + } + + // Move to next entry + offset += zip_header_size + header.filename_size + header.extra_field_size + header.size_uncompressed; + } + + return true; +} + + /******************************************************************** ** Encapsulation of FS and File to piggyback on Arduino ** diff --git a/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.h b/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.h index 7c41f7623..75ee4bfe1 100644 --- a/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.h +++ b/lib/libesp32/Zip-readonly-FS/src/ZipReadFS.h @@ -13,6 +13,25 @@ class ZipReadFSImpl; typedef std::shared_ptr ZipReadFSImplPtr; +/******************************************************************** +** Callback type for iterating over ZIP archive entries +** Parameters: +** filename: the filename (after '#' separator processing) +** user_data: user-provided context pointer +** Return: true to continue iteration, false to stop +********************************************************************/ +typedef bool (*ZipIteratorCallback)(const char *filename, void *user_data); + +/******************************************************************** +** Iterate over all files in a ZIP archive +** Parameters: +** zipfile: an open File object pointing to the ZIP archive +** callback: function to call for each file entry +** user_data: user-provided context pointer passed to callback +** Returns: true if archive was parsed successfully +********************************************************************/ +bool ZipArchiveIterator(File &zipfile, ZipIteratorCallback callback, void *user_data); + class ZipReadFSImpl : public FSImpl { public: diff --git a/lib/libesp32/berry_tasmota/src/be_port.cpp b/lib/libesp32/berry_tasmota/src/be_port.cpp index c48765077..79b919c2c 100644 --- a/lib/libesp32/berry_tasmota/src/be_port.cpp +++ b/lib/libesp32/berry_tasmota/src/be_port.cpp @@ -100,6 +100,57 @@ BERRY_API void be_writebuffer(const char *buffer, size_t length) // provides MPATH_ constants #include "be_port.h" + +#ifdef USE_UFILESYS +// Callback context for listing archive files +struct ZipListContext { + bvm *vm; +}; + +// Callback function for ZipArchiveIterator +static bool _zip_list_callback(const char *filename, void *user_data) { + ZipListContext *ctx = (ZipListContext *)user_data; + be_pushstring(ctx->vm, filename); + be_data_push(ctx->vm, -2); + be_pop(ctx->vm, 1); + return true; // continue iteration +} + +// Helper function to list files in a ZIP archive +// Returns true if the path ends with '#' and archive listing was attempted +// The list object should already be on the Berry stack +static bool _be_list_archive_files(bvm *vm, const char *path) { + size_t path_len = strlen(path); + if (path_len == 0 || path[path_len - 1] != '#') { + return false; // not an archive path + } + + // Extract the archive path (without the trailing '#') + char archive_path[path_len + 2]; + if (path[0] == '/') { + strncpy(archive_path, path, path_len - 1); + archive_path[path_len - 1] = '\0'; + } else { + archive_path[0] = '/'; + strncpy(archive_path + 1, path, path_len - 1); + archive_path[path_len] = '\0'; + } + + // Open the archive file + File zipfile = ffsp->open(archive_path, "r"); + if (!zipfile) { + return true; // path ends with '#' but archive not found, return empty list + } + + // Use ZipArchiveIterator from Zip-readonly-FS module + ZipListContext ctx = { vm }; + ZipArchiveIterator(zipfile, _zip_list_callback, &ctx); + + zipfile.close(); + return true; +} +#endif // USE_UFILESYS + extern "C" { // this combined action is called from be_path_tasmota_lib.c // by using a single function, we save >200 bytes of flash @@ -147,8 +198,14 @@ extern "C" { } break; case MPATH_LISTDIR: - be_newobject(vm, "list"); // add our list object and fall through + be_newobject(vm, "list"); // add our list object returnit = 1; + // Check if path ends with '#' for archive listing + if (_be_list_archive_files(vm, path)) { + // Archive listing handled, skip normal directory listing + break; + } + // Fall through to normal directory listing case MPATH_ISDIR: case MPATH_MODIFIED: { //isdir needs to open the file, listdir does not