楔子

为什么讲这个?很简单,因为做需求碰到了,没找到什么特别有用的最佳实践,这里分享一些自己的思路。

需求背景是最近在撸的一个编辑器,编辑器基于 Electron 实现,桌面端编辑类的软件有个存档就很正常了。

存档文件

归档文件,又作存档文件,是由一个或多个计算机文件以及元数据组成的文件,用于将多个数据文件收集到一个文件中,以便于传输和存储,或者压缩以减少存储空间。也称打包文件,归档并压缩时常称为压缩文件。通常会存储目录结构,错误检测与纠正信息,注释,有时还使用加密。

存档文件十分常见,最常见的如:

  • ZIP、RAR、TAR 等压缩包
  • PS 、AI、XD、PDF、SKetch 等设计文件
  • DOCX、XLSX、PPTX 等 office 存档

或者说一种文件格式就是一种存档表现,存档文件大多支持以下一个或多个特性

  • 将元数据 (文件名,权限等)存储在文件中
  • 校验和
  • 无损数据压缩
  • 多个文件存储在一个文件中
  • 文件更新 (用于增量备份)
  • 加密
  • 错误纠正
  • 文件分割,用于存储或传输

最好理解的就是 zip 文件,其支持了多个文件的存储、压缩、加密与校验(CRC 校验文件完整性),其也是很多存档文件包装的常用格式。

存档文件格式

咋一看不同软件存档格式都是不一样的,但其内部实现一般逃不出以下的套路:

  • 专有格式文件,其内部按文件规范以指定的规则(如字节区间)存储数据
  • 基于现有的文件格式做包装,通过修改文件后缀或编码来创建新格式

专有格式文件

这类存档文件一般由专业软件产生,其经过严格设计,比较典型的例子就是 Photoshop 所使用的 PSD 文件,其文件规范指定一系列字节区间数据定义。

附:Adobe Photoshop File Formats Specification。

其他类似的文件还有 PDF、FBX 及 Office 早期的存档文件 DOC、XLS、PPT 都为专有的二进制存档文件,这类专有存档格式依赖其开放的文件标准,没公开其文件规范则很难进行解析。

基于现有文件包装

鲁迅曾说过:

“这个世界上本没有那么多文件,改后缀的人多了,也便成了新文件”

很好理解,很多软件生成的存档文件不过是将常见的文件进行二次包装修改后缀所得,常用于包装的格式有:JSON、XML/HTML 与 ZIP。

基于 ZIP

Sketch 文件就是个很典型的例子,其文件本质就是一个 zip 文件,改后缀后可直接看到文件内容:

附:Sketch File format

还有就是常见的 Office 存档(DOCX、XLSX、PPTX…),其本质还是个 ZIP 包,文件的后缀中的 X 表示其内部文件描述是基于 Office Open XML 实现的。

基于 JSON

excalidraw 的存档文件(excalidraw)与 processon 的存档文件(pos)其都是基于单个 JSON 文件封装。

基于 XML/HTML

顺手扒了下语雀的存档文件(lake),其存档是基于单个 XML/HTML 实现的。

如何查看原始文件格式?

是否有方法可以快速知晓一个文件是否为包装格式?这时候就需要一个可以查看二进制内容的编辑器了,通过编辑器查看文件数据与组织结构,可以通过一些特定的标志判别出文件格式。

语雀 lake

processon pos

zip

对于 JSON 与 XML 一类的文本格式包装,通过 hex editor 是可以直接知晓内部数据结构的,但对于二进制文件而言,就需要一些特殊的文件标识来确定文件格式了。

以 ZIP 文件为例,其文件规范中一些文件头字段是固定的,如头部的 50 4B 03 04,这就是一个明显标识,我们可以通过其确定文件为压缩文件。

  • winodw hex editor:mh-nexus.de/en/hxd/
  • mac hex editor:hexfiend.com/

隐藏文件格式

当有人简单包装文件格式时,就一定会有人想把文件内容隐藏。

例如存档文件中涉及一些核心技术实现或是隐私数据,这时候隐藏存档文件内容就很重要了。

该如何实现呢?

上面讨论过了,文件存档不外乎两种思路:

  • 专有格式
  • 基于现有格式包装

专有格式的存档天然具有隐蔽性,只要不公开格式规范是很难破解存档信息的,当然其设计维护的成本也是比较高的。

包装类型的存档类型文件想要隐藏原始信息就需要对原始文件进行重新编码,以隐藏原始的格式特征。这里可以参考 Figma 存档文件(fig),其存档文件明显是经过编码处理的。

至于具体的编码规则可以自行定义,一般是将原始文件转为 Buffer/ArrayBuffer 再针对其字节编码,例如:

  • 逐字节与 255 相减,存其差值绝对值
  • 替换文件中一些特殊编码标识,例如替换 zip 文件的 50 4B 03 04
  • 在原始 buffer 中按规则插入一些特殊字节片段
  • 使用 AES、DES、RSA、DSA、ECC 等算法对 Buffer 进行加密
  • 取 buffer 不同片段进行不同编码

文件读取解析时使用相反操作即可,只要不惧加解密与读写的性能维护的成本,相信您一定可以设计出最为隐蔽的文件~

自定义存档文件实现

实际演示一个基于 Zip 文件封装文件的例子,先来实现 Zip 文件的读写:

import fs from 'fs';
import path from 'path';
import AdmZip from 'adm-zip';interface IArchiveFileWriteOptions {// 存档文件路径dest: string;files: Array<{// zip 文件内的文件路径dest: string;// 需要写入 zip 本地文件路径local?: string;// 需要写入 zip 数据source?: Buffer | string;}>;
}class ZipFile {async read(entry: string): Promise<AdmZip> {return new AdmZip(entry);}async write(options: IArchiveFileWriteOptions): Promise<void> {const { dest, files } = options;const zip = new AdmZip();// 往 zip 容器中写入文件files.forEach((file) => {const { dest: destName, source, local } = file;if (source) {if (Buffer.isBuffer(source)) {zip.addFile(destName, source);return;}zip.addFile(destName, Buffer.from(source, 'utf-8'));return;}if (local) {zip.addLocalFile(local, destName);return;}});const zipFileBuffer = await zip.toBufferPromise();await fs.promises.writeFile(dest, zipFileBuffer);}
}(async function main() {const zipFile = new ZipFile();const dest = path.resolve(__dirname, 'demo.myfile');await zipFile.write({dest,files: [{dest: 'content.text',source: '扶桑若木',},],});console.log('write:', dest);const zipRes = await zipFile.read(dest);console.log('content.text --->', zipRes.readAsText('content.text'));
})();

目前并未对 demo.myfile 进行加密处理,所以可以看到 zip 文件头的标识:

接下来针对原始 zip 文件做 AES 加密处理:

import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import AdmZip from 'adm-zip';
import { streamToBuffer, bufferToStream } from './src/utils/stream';import type { Transform } from 'stream';interface IMyFileWriteOptions {// 存档文件路径dest: string;files: Array<{// zip 文件内的文件路径dest: string;// 需要写入 zip 本地文件路径local?: string;// 需要写入 zip 数据source?: Buffer | string;}>;
}class MyCipher {algorithm: string = 'aes-128-cbc';password: string = '0000111122223333';salt: string = '0000111122223333';iv: string = '0000111122223333';get keyBuffer(): Buffer {return crypto.scryptSync(this.password, this.salt, 16);}get ivBuffer(): Buffer {return Buffer.from(this.iv, 'utf-8');}async createEncipher(): Promise<Transform> {return crypto.createCipheriv(this.algorithm, this.keyBuffer, this.ivBuffer);}async createDecipher(): Promise<Transform> {return crypto.createDecipheriv(this.algorithm, this.keyBuffer, this.ivBuffer);}
}class MyFile {private MyCipher = new MyCipher();async read(entry: string): Promise<AdmZip> {const decipher = await this.MyCipher.createDecipher();const readStream = fs.createReadStream(entry);// 读取文件流 -> 解密const zipBuffer = await streamToBuffer(readStream.pipe(decipher));return new AdmZip(zipBuffer);}async write(options: IMyFileWriteOptions): Promise<void> {const { dest, files } = options;const zip = new AdmZip();// 往 zip 容器中写入文件files.forEach((file) => {const { dest: destName, source, local } = file;if (source) {if (Buffer.isBuffer(source)) {zip.addFile(destName, source);return;}zip.addFile(destName, Buffer.from(source, 'utf-8'));return;}if (local) {zip.addLocalFile(local, destName);return;}});const zipFileBuffer = await zip.toBufferPromise();const encipher = await this.MyCipher.createEncipher();const writeStream = fs.createWriteStream(dest);return new Promise((resolve) => {// zip buffer -> 加密 -> 写入文件bufferToStream(zipFileBuffer).pipe(encipher).pipe(writeStream).on('close', () => {resolve();});});}
}(async function main() {const myFile = new MyFile();const dest = path.resolve(__dirname, 'demo.myfile');await myFile.write({dest,files: [{dest: 'content.text',source: '扶桑若木',},],});console.log('write:', dest);const zipRes = await myFile.read(dest);console.log('content.text --->', zipRes.readAsText('content.text'));
})();

Zip 文件头已经看不到了~

存档文件清单

虽然讨论了很多关于存档文件包装与编码的实现,但实际针对存档内容组织也是很重要的一环,例如:

  • 一个 zip 文件该放哪些东西
  • 文件目录结构如何组织
  • 是否需要放置文件清单(manifest)、文件签名(sign)与版本文件(version)等

这些都需要详细设计,考虑后期升级与版本管理之类的操作~

其他

一些文件格式参考

讲讲存档文件的包装设计相关推荐

  1. 平面包装设计怎么制作_从平面文件中获取数据时如何避免包装设计缺陷

    平面包装设计怎么制作 As developers of SQL Server Integrations Services (SSIS) solutions, we have more than lik ...

  2. 电子烟包装设计都有些什么流程?

    电子烟包装设计的流程一般包括以下几个步骤: 1. 需求分析:明确电子烟产品的定位.目标用户.市场需求等关键信息,以及包装设计所需的基本要素,包括包装形式.包装材质.印刷方式等等,以便为后续的设计提供清 ...

  3. 三年级计算机课教案文档,小学三年级信息技术第十三课文件和文件夹教学设计...

    小学三年级信息技术院 第十三课 文件和文件夹教学设计 一.教材分析 知识点:文件的生成.保存与命名:文件夹及其创建.命名与改名. 知识结构 文件 概念 生成 保存 命名 文件夹 概念 创建 命名 改名 ...

  4. nbiot开发需要掌握什么_包装设计需要掌握什么技巧

    原标题:包装设计需要掌握什么技巧 现在是一个讲究"美"的时代,不仅要产品要美,还要有精美的包装设计来吸引消费者.好的设计能在短时间内吸引消费者的注意力,有助于提高商品的销售水平.那 ...

  5. 以QQ传输文件为例-设计测试用例

    功能:QQ传输文件为例设计用例观察点 要点说明: 1.QQ支持的文件大小是否均能正常传送 2.QQ支持传送的文件类型是否均可以传送 3.手动是否能将要传送的文拖拽至QQ窗口 4.手动将要传送的文件拖至 ...

  6. VC中海量文件读写类设计与应用(转)

    VC中海量文件读写类设计与应用   沈瑞冰 摘要 本文阐述了海量文件读写的一般方法,并分析了该方法中存在的内存耗尽问题和解决办法,并就此设计了一个海量文件读写类,封装了海量文件读写操作,最后给出了一个 ...

  7. 包装设计中文字字体的logo设计要注意什么

    包装设计中文字字体的logo设计要注意什么 设计字体的目的,是要使文字既具有充分传达信息的功能,又与产品形式.产品功能:人们的审美观念达到和谐和统一.一般可根据以下几个原则进行设计. (1)要符合包装 ...

  8. 江小白包装设计原型_雪碧和江小白的品牌跨界合作之旅可谓是一场品牌包装的视觉盛宴...

    大家好,我是古小一,一个行走在酒水品牌包装设计不归路上的小编! 当下品牌间的跨界合作越来越多,消费者不但有审美疲劳的趋势,脑洞过大的跨界还容易引发群嘲.不过好在有热情网友的帮助,雪碧与江小白已经自然地 ...

  9. CANOpen数据存档文件

    数据存档文件,用于保存所有从节点的字典配置. 在从节点初始化时,从节点将上报boot_up报文.主节点收到boot_up报文后,将对从节点的字典和数据存档文件进行对比,如果不匹配,则需要通过sdo报文 ...

最新文章

  1. C语言 字符串和字符串数组动态分配及赋值
  2. java synchronized关键字
  3. 快速准备电子设计大赛
  4. Java基础——注解
  5. 糟糕程序员的20个坏习惯
  6. SAP CRM WebClient UI表格编辑模式的调试明细
  7. 文件得编码和文件名的编码是不一样的
  8. 冲刺阶段一 11.15--11.21
  9. java调用cmd_Java调用CMD命令
  10. Fedora10使用若干问题
  11. html学习文档-8、HTML 图像
  12. 064 import和from...import
  13. java期末考试工程项目_java web 期末项目实验源码20套,自用学习非常不错!
  14. Java实现在线打开word文档加盖印章/盖章/签名功能
  15. 计算机和材料成型及控制工程,材料成型及控制工程专业属于什么门类
  16. Unity Shader 伽马校正详解
  17. E - Eddy的难题
  18. LWN:让内核支持符合FIPS规范的随机数!
  19. VS Code PHP代码提示和格式化插件 IntelliSense安装使用
  20. 数据脱敏:保障数据安全的脱敏方案

热门文章

  1. 索爱X9挂脖式蓝牙耳机,佩戴轻盈音质纯正真正享受音乐世界
  2. Fluid 给数据弹性一双隐形的翅膀 -- 自定义弹性伸缩,mysql基础教程
  3. 百度业务运营部_数据分析师(产品运营)岗位要求详解(1)
  4. python立体爱心代码_以下行为属于生涯发展问题的是。
  5. python云顶之翼
  6. 17:Polly与HttpClientFactory
  7. 【Kafka】(四)Kafka使用 Consumer 接收消息消费
  8. 五种方法教你解除电脑开机密码
  9. 新疆画家扬笛《怒放丝路双 2》人物画赏析
  10. 单片机按一下灯亮,按一下灯灭