Berry 'path.listdir(file.tapp#)' to list directory inside '.tapp' archives (#24367)

This commit is contained in:
s-hadinger
2026-01-18 23:19:40 +01:00
committed by GitHub
parent 00bd04f5c4
commit 9c73b27cbc
4 changed files with 148 additions and 1 deletions

View File

@@ -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

View File

@@ -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
**

View File

@@ -13,6 +13,25 @@
class ZipReadFSImpl;
typedef std::shared_ptr<FSImpl> 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:

View File

@@ -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