65.9K
CodeProject 正在变化。 阅读更多。
Home

zip.hpp:用于物联网的 Zip 查看器和解压库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (14投票s)

2021 年 7 月 20 日

MIT

4分钟阅读

viewsIcon

16859

downloadIcon

223

在内存受限的环境中浏览和提取 zip 文件

引言

我需要在 Arduino 框架和其他物联网框架下解压 zip 文件。问题是,我搜索了很多地方,但没有找到一个能够适应内存受限环境(尤其是 Arduino 框架)的合适库。

构建这个大杂烩

您需要安装了 PlatformIO 的 VS Code。您需要一个已连接的 ESP32。在首次构建之前,您需要上传文件系统镜像以设置 SPIFFS 分区。除非您更换正在使用的 ESP32,否则无需再次执行此操作。从平台来看,您几乎可以在任何地方使用 *zip.hpp* / *stream.hpp* / *bits.hpp*,但我只在 ESP32 上对其进行了测试。

编写这个混乱的程序

读取 zip 归档文件相当容易,虽然有点奇怪,因为您需要的很多相关信息是相对于文件末尾而不是开头而言的。不幸的是,这使得无法进行前向流式读取。关于它的另一件事是,Huffman 解码需要能够回溯到输出缓冲区(是的,您已经写入的解压数据)的 32kB 处,因为它会使用这些数据进行进一步的解压。

这种回溯能力使事情变得复杂。

最初,我实现了一种无缓冲的回溯机制,它使用可搜索的输出流而不是 32kB 的 RAM 窗口,但在回溯 Arduino File 对象时遇到了问题。出于某种原因,它根本不起作用。另一个问题是这种技术速度慢得多。

在此之后,我打算实现一个按需解压流,这样您就可以在开始查看之前不必解压整个流。然而,这样做需要更多的内存。我可能会在以后选择性地实现它,但这样做会大大增加代码体积。

所以,在经历了所有这一切之后,我最终采用了一种方法,该方法接受一个可读输入流和一个可写输出流来执行 Huffman 解压缩操作,并使用堆上的一个临时的 32kB 缓冲区来完成。我真的很讨厌在物联网设备上分配如此多的 RAM,但除非我能找出并解决 Arduino File 对象的问题,否则我无能为力。

鉴于以上原因,最终您几乎肯定需要一个连接的 SD 读写器,除非您拥有像带 SPIFFS 分区的 ESP32 这样的设备,以便存储解压后的流。

另一个问题是 Arduino File 对象本身就占用大量堆栈空间,所以即使 zip 归档结构不占用太多空间,您仍然需要将大部分工作对象分配为全局变量或在堆上分配。

最后,如果以上内容还没有说清楚,您至少需要一个中等配置的物联网设备才能运行此代码。例如,小小的 ATMega2560 只有 8kB 的 RAM,所以它根本无法胜任。我在带有 512kB RAM(有效可用约 300kB)的 ESP32 WROOM 上测试了此代码,这比运行它所需的多得多,但它是我常用的平台。

现在,在排除了所有免责声明之后,让我们更深入地了解一下我所做的工作。

我数学很差,所以我厚颜无耻地从 这里 借鉴了一些公共领域的源代码来执行解压缩。顺便说一句,那个 Git 仓库简直是神奇,所以请收藏它。那里的所有头文件都是宝藏!

除此之外,我只是通读了 zip 文件,寻找关键部分来提取归档信息。请注意,并非归档中的所有流都经过压缩,因此 archive_entry.extract() 背后有两种不同的提取机制。

使用这个烂摊子

这个库简短而精炼,使用起来非常简单。

我将在此处进行一次代码转储,这应该能解释一切。请注意,此特定代码是 ESP32 特定的。

#include <Arduino.h>
// DID YOU UPLOAD THE FILESYSTEM IMAGE YET?
#include <SPIFFS.h>
#include <stream.hpp>
#include <zip.hpp>
using namespace io; // for streams
using namespace zip; // for zips

// if we try to declare all this
// on the stack we run into problems
// apparently the File class is pretty
// heavy:
char path[1024];
File f;
File f2;
archive arch;
archive_entry entry;

void setup() {
  Serial.begin(115200);
  Serial.println();

  SPIFFS.begin(false);
  f=SPIFFS.open("/frankenstein.epub","rb");
 if(!f) {
    Serial.println("File not found");
    while(true);
  }
  file_stream fs(f);
  if(zip_result::success!=archive::open(&fs,&arch)) {
    Serial.println("Zip load failed.");
    while(true);
  }
  Serial.print("Number of files ");
  Serial.println(arch.entries_size());
  
  arch.entry(11,&entry);
  
  if(SPIFFS.exists("/tmp.htm")) {
    SPIFFS.remove("/tmp.htm");
  }

  f2 = SPIFFS.open("/tmp.htm","wb");
  
  file_stream fs2(f2);
  Serial.print("extracting ");
  entry.copy_path(path,1024);
  Serial.print(path);
  Serial.println("...");
  zip_result rr=entry.extract(&fs2);
  if(zip_result::success!=rr) {
    Serial.print("extraction failed ");
    Serial.println((int)rr);
    while(true);
  }
  Serial.println("extraction complete");
  f.close();
  f2.close();
  f=SPIFFS.open("/tmp.htm","rb");
  if(!f) {
    Serial.println("Temp file not found");
    while(true);
  }
  while(true) {
    int i = f.read();
    if(0>i) break;
    Serial.write(i);
  }
  f.close();
  
}

void loop() {
}

唯一可能令人困惑的是将 File 对象封装到 file_stream 中。本质上,zip 库不了解 Arduino 的 File,但它了解 io::stream。这样做是为了让库保持跨平台性。之所以不使用 std::iostream<> 模板类,是因为 STL 并非在所有平台上都完全可用。

另一个需要注意的事情是,我们使用 copy_path() 来从归档条目中获取路径。我的物联网库极不愿意在堆上动态分配内存,除非绝对必要。它通常会把内存分配留给您,而这正是它在这里所做的,所以使用它就像使用 sprintf() 一样,它接受一个缓冲区和一个最大尺寸。

不要指望它速度很快。Zip 文件最初并不是为物联网设备设计的。不过,当您需要它时,您就真的需要它了。

请注意,此代码未来可能不会作为独立库进行维护。我将其整合到我即将发布的名为 UIX 的用户界面框架中,并将专注于该代码库。

历史

  • 2021 年 7 月 20 日 - 初次提交
© . All rights reserved.