[TOC]

学习笔记

HPatchLite\HDiffPatch\libHDiffPatch\HPatchLite\hpatch_lite.c

Hptach 算法

1. Hptach 的核心:加法差分 (Additive Diff)

这种方法可以被称为加法差分增量差分

  • 核心公式: NewData = OldData + DiffData
  • 反向公式: DiffData = NewData - OldData (在生成补丁时计算)

工作流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
                  +----------------------+
生成补丁时 (hpatch_diff) | |
| 新文件 (New File) |
+----------------------+
| (减法)
v
+----------------------+
| 旧文件 (Old File) |
+----------------------+
|
v
+-------------------------------------------------------------------------+
| 差分数据 (DiffData) |
| (大部分是0或接近0的小数值,代表“无变化”或“微小变化”,非常适合压缩) |
+-------------------------------------------------------------------------+


+----------------------+
应用补丁时 (hpatch_patch) | |
| 旧文件 (Old File) |
+----------------------+
| (加法)
v
+----------------------+
| 差分数据 (DiffData) |
+----------------------+
|
v
+-------------------------------------------------------------------------+
| 新文件 (New File) |
| (被成功还原) |
+-------------------------------------------------------------------------+

为什么这种方法很有效?

因为在多数文件更新中(无论是程序、文档还是图片),大部分内容是不变的,或者只有微小的改动

  • NewData[i] == OldData[i] 时,DiffData[i] 就是 0
  • NewData[i]OldData[i] 非常接近时,DiffData[i] 就是一个很小的正数或负数

这导致 DiffData 中充满了大量的 0 和小数值,这种数据的非常低,因此可以被像 zlib、LZMA 这样的通用压缩算法极大地压缩。Hptach 正是利用了“加法差分”与“通用压缩”的组合拳来获得极高的压缩率。


2. 其他类型的差分编码

“差分编码”是一个广义的概念,其核心思想是“不记录数据本身,而是记录数据与某个参考(基准)之间的差异”。加法只是实现这种思想的一种方式。

以下是其他几种常见的差分编码方式:

a. 复制/指令式差分 (Copy/Instruction-based Diff)

这是最著名的一种,以 rsyncbsdiff 算法为代表。它的核心不是字节运算,而是生成一系列指令

  • 核心指令:

    1. COPY(old_pos, length): 从旧文件的 old_pos 位置复制 length 长度的数据到新文件。
    2. ADD(diff_pos, length): 从补丁文件的 diff_pos 位置复制 length 长度的新数据到新文件。
  • 优点: 对于数据块发生移动(例如,在代码中移动一个函数)、大段重复大段删除的情况,这种方法极为高效。它只需要记录一条“复制”指令,而不需要记录大量字节的加法差分。因此,bsdiff 通常能生成比 Hptach 更小的补丁包。

  • 缺点: 算法更复杂,通常需要更多的内存,并且在应用补丁时速度可能比纯粹的加法差分要慢。

b. 异或差分 (XOR Diff)

这种方法在某些场景下也很有用,尤其是在处理二进制数据或需要可逆操作时。

  • 核心公式: NewData = OldData ^ DiffData
  • 优点: 异或操作有一个独特的对称性:A ^ B = C,那么 A ^ C = B。这意味着打补丁和制作补丁可以用完全相同的操作,非常优雅。
  • 缺点: 对于很多数据类型,异或后的差分数据不一定比加法差分的压缩率更高。

总结对比

差分方法 核心操作 优点 缺点 典型应用
加法差分 (Hptach) New = Old + Diff 算法简单,速度极快,对微小改动友好,差分数据压缩率高 对数据块移动不敏感,可能不是最小的补丁 Hptach, Google Courgette
指令式差分 (bsdiff) COPY(old)ADD(diff) 指令集 对数据块移动、复制、删除极为高效,通常能生成最小的补丁 算法复杂,内存消耗大,速度相对较慢 bsdiff, rsync, Git 的 pack 文件
异或差分 (XOR) New = Old ^ Diff 操作对称可逆,实现简单 差分数据的可压缩性不一定最优 二进制文件版本控制,一些数据库增量备份

结论:

您看到的“按位相加”是 Hptach 所选择的差分编码核心。Hptach 的设计哲学是在速度、内存占用和补丁大小之间寻求一个出色的平衡点。它通过极其快速和简单的加法运算,生成高度可压缩的差分数据,从而在大多数场景下都能获得非常好的性能和效果。

hpatch_lite_open Hpatch Lite 头部数据解析

  • 负责打开一个补丁文件(diff_data),并解析其文件头,以获取补丁应用所必需的元数据

原理与设计思路解析

hpatch_lite_open 函数的核心职责是验证并解析 Hpatch Lite 格式的补丁文件头部。这个头部是一个紧凑的、固定大小(hpi_kHeadSize,通常为4字节)的数据块,包含了应用补丁所需的所有初始信息。

  • 文件头结构 (Header Format)
    Hpatch Lite 的文件头设计得非常紧凑,在一个4字节的头部中编码了多种信息,其结构如下:
字节偏移 字段名称 长度 (字节) 描述
0 Magic Byte 1 1 固定为 'h',用于文件类型识别。
1 Magic Byte 2 1 固定为 'I',与'h'共同组成 “hI” 魔法标识。
2 compress_type 1 压缩类型。标识补丁数据使用了哪种压缩算法(如 zlib, bzip2, lzma 等)。
3 Packed Info 1 一个被打包的字节,通过位运算存储了3种不同的信息。
  • Packed Info 字节 (第3字节) 的详细解析:
    这个字节被巧妙地划分成三个部分:

    • 高2位 (Bits 6-7): (byte >> 6)
      • 版本号 (Version Code): 用于标识 Hpatch Lite 格式的版本,确保兼容性。代码会检查它是否等于预定义的 kHPatchLite_versionCode
    • 中3位 (Bits 3-5): ((byte >> 3) & 7)
      • uncompressSize 的字节数: 表示解压后(或原始)数据的大小值本身占用了多少个字节(0-7字节)。
    • 低3位 (Bits 0-2): (byte & 7)
      • newSize 的字节数: 表示应用补丁后新文件的大小值本身占用了多少个字节(0-7字节)。
  • 数据读取流程

    1. 读取固定头部: 首先,函数读取固定长度(hpi_kHeadSize)的头部数据。
    2. 校验与解析:
      • 校验文件魔法标识 'hI' 和版本号,如果不对,则说明不是一个有效的或兼容的 Hpatch Lite 文件,解析失败。
      • 直接读取压缩类型。
      • 通过位运算从 Packed Info 字节中解包newSizeuncompressSize 这两个字段的长度
    3. 读取可变长数据: 根据上一步解析出的长度,函数接着从数据流中分别读取 newSizeuncompressSize 的实际数据。例如,如果解析出 newSize 的长度是4字节,它就会再读取4个字节。
    4. 字节到整数的转换 (_hpi_readSize):
      • 由于 newSizeuncompressSize 是以多个字节的形式存储的,需要一个辅助函数 _hpi_readSize 将这些字节转换成一个整数。
      • 这个函数的实现方式 v=(v<<8)|(hpi_pos_t)buf[len] 并且 len 从高到低递减,表明它将字节数组视为 小端序 (Little-Endian) 来解析。也就是说,缓冲区中的第一个字节 buf[0] 是整数的最低位字节 (LSB)。
  • 设计优点

    • 紧凑: 将多个元数据打包进一个字节,节省了头部空间。
    • 灵活: newSizeuncompressSize 的长度是可变的,对于较小的文件,可以用1或2个字节来表示其大小,从而节省空间;对于非常大的文件,最多可以使用8个字节(64位)来表示,扩展性强。

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* @brief 从字节缓冲区中读取一个小端序存储的整数。
* @param buf 存放整数数据的缓冲区。
* @param len 整数占用的字节数。
* @return hpi_pos_t 返回转换后的整数值。
*/
static hpi_pos_t _hpi_readSize(const hpi_byte* buf,hpi_size_t len){
hpi_pos_t v=0; // 初始化结果为0。
// 从 len-1 到 0 循环,即从缓冲区的末尾向前读取。
while(len--){
// 核心逻辑:
// 1. v << 8: 将当前结果左移8位,为下一个字节腾出空间。
// 2. | (hpi_pos_t)buf[len]: 将当前字节(buf[len])放到 v 的低8位上。
// 因为 len 是从高到低变化的,所以 buf[len-1]...buf[0] 依次成为整数的 MSB...LSB。
// 举例:buf=[0x12, 0x34], len=2.
// 第一次循环 (len=1): v = (0<<8) | buf[1] = 0x34
// 第二次循环 (len=0): v = (0x34<<8) | buf[0] = 0x3400 | 0x12 = 0x3412
v=(v<<8)|(hpi_pos_t)buf[len];
}
return v;
}

/**
* @brief 打开并解析一个 Hpatch Lite 格式的补丁流,读取其头部信息。
* @param diff_data 补丁数据流的句柄。
* @param read_diff 用于从流中读取数据的函数指针。
* @param out_compress_type 输出参数,用于返回解析出的压缩类型。
* @param out_newSize 输出参数,用于返回补丁应用后的新文件大小。
* @param out_uncompressSize 输出参数,用于返回解压缩后数据的大小(如果适用)。
* @return hpi_BOOL 成功返回 hpi_TRUE,失败返回 hpi_FALSE。
*/
hpi_BOOL hpatch_lite_open(hpi_TInputStreamHandle diff_data,hpi_TInputStream_read read_diff,
hpi_compressType* out_compress_type,hpi_pos_t* out_newSize,hpi_pos_t* out_uncompressSize){
// 定义版本号常量。
#define kHPatchLite_versionCode 1

// lenn 和 lenu 用于临时存储长度信息。
hpi_size_t lenn=hpi_kHeadSize; // 期望读取的头部长度。
hpi_size_t lenu;

// 定义一个缓冲区。其大小取 hpi_kHeadSize 和 sizeof(hpi_pos_t) 中的较大者,
// 以确保该缓冲区既能装下固定头部,也能装下后续读取的变长整数。
hpi_byte buf[(hpi_kHeadSize>sizeof(hpi_pos_t))?hpi_kHeadSize:sizeof(hpi_pos_t)];

// 通过函数指针 read_diff 读取固定长度的头部到 buf 中。_CHECK 是一个错误检查宏。
_CHECK(read_diff(diff_data,buf,&lenn));

// --- 开始校验和解析头部 ---

// buf[3] 是包含多种信息的 "Packed Info" 字节。
lenu=buf[3];

// _SAFE_CHECK 是一个安全校验宏,用于验证头部数据的合法性。
// 1. lenn==hpi_kHeadSize: 确认是否成功读取了预期长度的头部。
// 2. (buf[0]=='h')&(buf[1]=='I'): 校验文件魔法标识是否为 "hI"。
// 3. ((lenu>>6)==kHPatchLite_versionCode): 将 buf[3] 右移6位取出高2位,校验版本号。
_SAFE_CHECK((lenn==hpi_kHeadSize)&(buf[0]=='h')&(buf[1]=='I')&((lenu>>6)==kHPatchLite_versionCode));

// buf[2] 直接存储了压缩类型。
*out_compress_type=buf[2];

// 从 "Packed Info" 字节中解包长度信息。
// (lenu & 7): 取 buf[3] 的低3位,得到 newSize 字段的字节数。
lenn=lenu&7;
// ((lenu >> 3) & 7): 将 buf[3] 右移3位再取低3位,得到 uncompressSize 字段的字节数。
lenu=(lenu>>3)&7;

// 校验解析出的长度是否合法(不能超过 hpi_pos_t 类型的最大字节数)。
_SAFE_CHECK((lenn<=sizeof(hpi_pos_t))&(lenu<=sizeof(hpi_pos_t)));

// --- 读取可变长度的元数据 ---

// 根据刚刚解析出的长度 lenn,读取 newSize 的数据。
_CHECK(read_diff(diff_data,buf,&lenn));
// 调用 _hpi_readSize 将读取到的字节数据转换为整数,并存入输出参数。
*out_newSize=_hpi_readSize(buf,lenn);

// 根据刚刚解析出的长度 lenu,读取 uncompressSize 的数据。
_CHECK(read_diff(diff_data,buf,&lenu));
// 同样,转换为整数并存入输出参数。
*out_uncompressSize=_hpi_readSize(buf,lenu);

// 所有操作成功,返回真。
return hpi_TRUE;
}

_cache_unpackUInt 可变长度整数解码

原理与设计思路解析

这段代码的核心是实现了一个高效的输入缓存(Input Cache)机制,并在此基础上提供了一个用于解码可变长度整数的工具函数。这是一个设计精良、高效且可扩展的底层数据读取模块。

  • 设计目的:性能优化

    • 操作系统和硬件的I/O操作(如读文件、收网络包)通常是昂贵的、有延迟的。频繁地进行单字节的I/O请求会导致性能低下。
    • _hpi_TInputCache 结构体通过引入一个内存缓冲区 cache_buf,完美地解决了这个问题。它通过 _cache_update 函数一次性从底层输入流 inputStream 读取一大块数据到内存中。
    • 之后,_cache_read_1byte 等函数就可以直接从高速的内存中逐字节地提供数据,只有当内存缓冲区耗尽时,才需要再次进行真正的I/O操作。这大大减少了系统调用的次数,提升了数据读取效率。
  • 数据结构 (_hpi_TInputCache)

    • 这是一个典型的”生产者-消费者”模型的简化版。_cache_update 是生产者,负责填充数据。_cache_read_1byte 是消费者,负责消耗数据。
    • cache_begincache_end 两个索引(或指针)是管理缓冲区的关键。它们定义了一个“窗口” [cache_begin, cache_end),表示缓冲区中当前有效的、尚未被读取的数据范围。当 cache_begin == cache_end 时,意味着缓冲区已被读空。
  • 核心函数与技巧

    • _cache_update (填充器): 这是缓存机制的“后勤”。它被设计为在缓存为空时被动调用,其职责单一且明确:填充缓存并重置状态。它通过函数指针 read_code 的设计,将缓存管理逻辑与具体的I/O实现(读文件、读网络等)解耦,使得这套缓存机制可以适用于任何类型的输入流。

    • _cache_read_1byte (读取器): 这是提供给上层逻辑的主要接口。它对上层屏蔽了复杂的缓存管理细节。goto 语句在此处的使用是一种性能优化技巧,用于避免在成功更新缓存后进行函数递归调用或重复编写逻辑,这在底层的C代码中很常见。

    • _cache_unpackUInt (解码器) 与 VLQ/LEB128 详解:
      这个函数揭示了 Hptach 所处理数据的一种底层格式。它实现了一种名为可变长度数量 (Variable-length quantity, VLQ)LEB128 (Little-Endian Base 128) 的编码方案。

      1. 核心思想
      用可变数量的字节来表示一个整数。对于数值较小的整数,使用较少的字节(通常是1个字节);对于数值较大的整数,使用较多的字节。因为在大多数数据中,小数值的出现频率远高于大数值,所以这种方法能有效地压缩数据。

      2. 编码规则

      • 每个字节提供 7位 (bit) 用来存储数据。
      • 每个字节的最高位 (Most Significant Bit, MSB) 作为“连续标志位”。
      • 如果 MSB 是 1,表示后面还有后续的字节属于同一个整数。
      • 如果 MSB 是 0,表示这是这个整数的最后一个字节。

      3. 编码示例:数字 300
      我们来看如何将整数 300 编码成这种格式:

      • 二进制表示: 300 的二进制是 0000 0001 0010 1100
      • 分组: 将二进制从右到左按7位一组进行切分。
        • 第一组 (低位): 010 1100 (十进制为 44)
        • 第二组 (高位): 000 0010 (十进制为 2)
      • 添加标志位: 我们需要两个字节来存储。第一个字节的标志位设为1,第二个(最后一个)字节的标志位设为0
        • 第一个字节: 1 (标志位) + 000 0010 (高位数据) -> 10000010 (十六进制 0x82)
        • 第二个字节: 0 (标志位) + 010 1100 (低位数据) -> 00101100 (十六进制 0x2C)
      • 所以,整数 300 最终被编码为字节流:0x82 0x2C

      4. 解码流程 (即 _cache_unpackUInt 函数的逻辑)
      现在,我们模拟函数如何从字节流 0x82 0x2C 中解码出 300
      假设 v=0, isNext=1 (初始调用时)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      +---------------------------------+
      | 开始解码 |
      | v = 0, isNext = 1 |
      +---------------------------------+
      |
      +---------------------------------+
      | 循环: while(isNext) |
      | (isNext is 1) |
      +---------------------------------+
      |
      v
      +---------------------------------+
      | b = _cache_read_1byte() |
      | (读取第一个字节 0x82) |
      +---------------------------------+
      |
      v
      +---------------------------------+
      | v = (v << 7) | (b & 127) |
      | v = (0 << 7) | (0x82 & 127) |
      | v = 0 | 2 => v = 2 |
      +---------------------------------+
      |
      v
      +---------------------------------+
      | isNext = b >> 7 |
      | isNext = 0x82 >> 7 => 1 |
      +---------------------------------+
      |
      +---------------------------------+ <---+
      | 循环: while(isNext) | | (回到循环顶部)
      | (isNext is 1) | |
      +---------------------------------+ |
      | |
      v |
      +---------------------------------+ |
      | b = _cache_read_1byte() | |
      | (读取第二个字节 0x2C) | |
      +---------------------------------+ |
      | |
      v |
      +---------------------------------+ |
      | v = (v << 7) | (b & 127) | |
      | v = (2 << 7) | (0x2C & 127) | |
      | v = 256 | 44 => v = 300 | |
      +---------------------------------+ |
      | |
      v |
      +---------------------------------+ |
      | isNext = b >> 7 | |
      | isNext = 0x2C >> 7 => 0 | |
      +---------------------------------+ |
      | |
      +---------------------------------+ ----+
      | 循环: while(isNext) |
      | (isNext is 0) |
      +---------------------------------+
      |
      v
      +---------------------------------+
      | 返回 v |
      | (返回 300) |
      +---------------------------------+

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 定义一个名为 _hpi_TInputCache 的结构体,用于管理带缓冲的输入流。
// 这个结构体的核心思想是:一次从底层输入流(如文件、网络)读取一大块数据到内存缓冲区(cache_buf),
// 然后上层调用者每次只从这个缓冲区中取数据,从而减少昂贵的I/O操作次数。
typedef struct _hpi_TInputCache{
hpi_size_t cache_begin; // 缓冲区域中,当前未读数据的起始位置(索引)。
hpi_size_t cache_end; // 缓冲区域中,已加载数据的结束位置(索引),即有效数据末尾+1。
hpi_byte* cache_buf; // 指向实际存储数据的内存缓冲区的指针。
hpi_TInputStreamHandle inputStream; // 底层输入流的句柄(例如文件指针或socket描述符)。
hpi_TInputStream_read read_code; // 一个函数指针,指向真正从底层输入流(inputStream)读取数据的函数。
} _hpi_TInputCache;

// --- 类型和函数宏定义,用于简化代码 ---
#define _TInputCache _hpi_TInputCache // 为结构体类型创建别名。
#define _cache_read_1byte _hpi_cache_read_1byte // 为函数创建别名。
#define _cache_update _hpi_cache_update // 为函数创建别名。
#define _cache_success_finish _hpi_cache_success_finish // 为函数创建别名 (此片段中未使用)。

/**
* @brief 更新(填充)缓冲区。
* @param self 指向 _TInputCache 实例的指针。
* @return hpi_BOOL 类型,如果成功读取到新数据则返回真 (非0),否则返回假 (0)。
*
* 当缓冲区中的数据被读完时 (cache_begin == cache_end),此函数被调用。
* 它会调用 read_code 函数指针,从底层输入流读取新数据,并填充到 cache_buf 的开头。
*/
hpi_BOOL _cache_update(struct _TInputCache* self){
hpi_size_t len=self->cache_end;
// 断言:确保调用此函数时,缓冲区确实是空的。这是一个防御性编程措施。
assert(len==self->cache_begin);

// 调用函数指针 read_code 从底层流读取数据。
// 参数: self->inputStream 是流句柄,self->cache_buf 是目标缓冲区,&len 是期望读取的长度(同时也是返回实际读取的长度)。
// 如果读取失败,read_code 返回假,此时将 len 设为0。
if (!self->read_code(self->inputStream,self->cache_buf,&len))
len=0;

// 重置缓冲区的起止位置。
self->cache_begin=0; // 新数据从缓冲区的开头开始。
self->cache_end=len; // 数据的结束位置是实际读取到的长度。

// 如果 len 不为0,说明成功读取到了数据,返回真。
return len!=0;
}

/**
* @brief 从缓冲区中读取一个字节。
* @param self 指向 _TInputCache 实例的指针。
* @return hpi_fast_uint8 类型,返回读取到的字节。如果已到流末尾且无法再读取,则返回 0。
*
* 这是上层代码获取数据的基本接口。
*/
hpi_fast_uint8 _cache_read_1byte(struct _TInputCache* self){
// 首先检查缓冲区中是否还有未读数据。
if (self->cache_begin != self->cache_end){
__cache_read_1byte: // goto 标签
// 缓冲区非空,直接返回当前字节,并将起始位置后移一位。
return self->cache_buf[self->cache_begin++];
}

// 如果缓冲区已空,尝试调用 _cache_update 填充数据。
if(_cache_update(self))
// 如果填充成功,使用 goto 跳转到__cache_read_1byte标签处,重新执行读取操作。
// 这里使用 goto 是为了避免函数递归调用或重复写读取逻辑,是一种在C语言底层代码中常见的性能优化技巧。
goto __cache_read_1byte;
else
// 如果填充失败(意味着流已结束),返回 0。
return 0;
}

/**
* @brief 从流中解包一个可变长度编码的无符号整数 (VLQ/LEB128 类似格式)。
* @param self 指向 _TInputCache 实例的指针。
* @param v 初始值,通常是第一个字节解码后的值。
* @param isNext 一个标志,表示是否还有后续字节需要读取。
* @return hpi_pos_t 类型,返回完整解码后的整数。
*
* 这种编码方式可以用更少的字节表示小的数字,从而节省空间。
*/
static hpi_pos_t _cache_unpackUInt(_TInputCache* self,hpi_pos_t v,hpi_fast_uint8 isNext){
// 循环条件 isNext 由每个字节的最高位决定。
while (isNext){
// 读取下一个字节。
hpi_fast_uint8 b=_cache_read_1byte(self);
// 核心解码逻辑:
// 1. (b & 127): 取出当前字节的低7位作为数据。
// 2. (v << 7): 将之前已经解码出的值向左移动7位,为新的7位数据腾出空间。
// 3. |: 将新的7位数据合并到结果中。
v=(v<<7)|(b&127);
// 更新循环条件:
// b >> 7: 取出当前字节的最高位 (MSB)。如果MSB是1,表示后面还有后续字节,isNext为1,循环继续;
// 如果MSB是0,表示这是最后一个字节,isNext为0,循环结束。
isNext=b>>7;
}
return v;
}

_patch_copy_diff_patch_add_old_withClip 将上一层函数解析出的指令转化为对文件的具体读写操作

原理与设计思路解析

这两个函数是补丁应用过程中的两个基本操作原子,分别对应了我们之前提到的“新数据”和“覆盖数据”的处理。

  • _patch_copy_diff:处理“新数据”

    • 职责单一: 这个函数的唯一目标就是将补丁文件 (diff) 中的一段连续数据,原封不动地复制到新文件 (out_newData) 中。
    • 缓存感知 (Cache-Aware): 它并非逐字节复制,而是以“块”(chunk)为单位进行。它会首先检查 diff 的输入缓存区里有多少可用数据 (decodeStep),然后一次性处理整个可用数据块。这种块处理方式远比单字节I/O高效。
    • 流程: 不断地从 diff 缓存中取出数据块,写入到新文件,直到满足要求的 copyLength 为止。如果中途缓存耗尽,它会自动调用 _cache_update 来填充缓存。
  • _patch_add_old_withClip:处理“覆盖数据”

    • 双重功能: 这个函数通过 isNotNeedSubDiff 标志实现了两种操作模式,是 Hpatch 算法的一个关键实现:
      1. 差分合成模式 (isNotNeedSubDifffalse): 这是标准的差分补丁逻辑。它执行 NewData = OldData + DiffData 的操作。数据流是:从旧文件读一块数据到 temp_cache -> 从 diff 文件读一块差分数据 -> 调用 addData 将差分数据temp_cache 中 -> 将 temp_cache 的结果写入新文件。
      2. 旧数据复制模式 (isNotNeedSubDifftrue): 在某些情况下,新文件的一部分可能与旧文件的某部分完全相同。此时,就不需要在补丁文件中存储差分数据(因为差分都是0),只需记录一个“从旧文件某处复制一段数据过来”的指令即可。此时,这个函数的数据流简化为:从旧文件读一块数据到 temp_cache -> 直接将 temp_cache 的内容写入新文件。
    • temp_cache 的作用: 在这里,temp_cache 扮演了一个至关重要的“工作台”角色。所有来自旧文件的数据和来自补丁文件的差分数据,都会在这个临时的内存区域中进行合并运算,然后再被写入最终目标。
    • addData 函数: 这是一个被强制内联 (hpi_force_inline) 的辅助函数,因为它执行的是最频繁、最底层的字节加法操作。*dst++ += *src++ 是一个非常高效的C语言惯用法,编译器能将其优化为非常快速的机器指令。

总而言之,hpatch_lite_patch 负责“决策”(解析指令),而这两个函数负责“执行”(搬运和加工数据)。它们的设计共同保证了补丁过程的高效性和灵活性。

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* @brief 从 diff 缓存中直接复制一段数据到新文件流。
* @param out_newData 目标新文件流的监听器。
* @param diff 源 diff 数据流的输入缓存。
* @param copyLength 需要复制的总字节数。
* @return hpi_BOOL 成功返回 hpi_TRUE。
*/
static hpi_BOOL _patch_copy_diff(hpatchi_listener_t* out_newData,
_TInputCache* diff,hpi_pos_t copyLength){
// 只要还有数据需要复制,就继续循环。
while (copyLength>0){
// 计算当前 diff 缓存中还有多少可用的数据。
hpi_size_t decodeStep=diff->cache_end-diff->cache_begin;

// 如果缓存为空...
if (decodeStep==0){
_CHECK(_cache_update(diff)); // 填充缓存。
decodeStep=diff->cache_end-diff->cache_begin; // 重新计算可用数据量。
}

// 本次循环处理的数据量不能超过剩余需要复制的总量。
if (decodeStep>copyLength)
decodeStep=(hpi_size_t)copyLength;

// 调用回调函数,将缓存中的数据块写入新文件。
_CHECK(out_newData->write_new(out_newData,diff->cache_buf+diff->cache_begin,decodeStep));

// 更新缓存的读取位置和剩余需要复制的长度。
diff->cache_begin+=decodeStep;
copyLength-=(hpi_pos_t)decodeStep;
}
return hpi_TRUE;
}

/**
* @brief 一个强制内联的辅助函数,用于将两个字节数组按位相加。
* @param dst 目标数组,也是被修改的数组。
* @param src 源数组。
* @param length 数组长度。
*/
static hpi_force_inline void addData(hpi_byte* dst,const hpi_byte* src,hpi_size_t length){
while (length--) { *dst++ += *src++; }
}

/**
* @brief 从旧文件中读取数据,根据需要与 diff 数据相加,然后写入新文件。
* @param old_and_new 包含读旧文件和写新文件回调的监听器。
* @param diff diff 数据流的输入缓存。
* @param isNotNeedSubDiff 标志位,如果为真,则直接复制旧文件数据;否则,执行加法。
* @param oldPos 在旧文件中读取的起始位置。
* @param addLength 需要处理的总数据长度。
* @param temp_cache 用于暂存和运算的临时缓冲区。
* @return hpi_BOOL 成功返回 hpi_TRUE。
*/
static hpi_BOOL _patch_add_old_withClip(hpatchi_listener_t* old_and_new,_TInputCache* diff,hpi_BOOL isNotNeedSubDiff,
hpi_size_t oldPos,hpi_size_t addLength,hpi_byte* temp_cache){
// 只要还有数据需要处理,就继续循环。
while (addLength>0){
hpi_size_t decodeStep;

// 根据是否需要差分数据,来确定本次处理块的大小。
if (!isNotNeedSubDiff){
// 需要差分:块大小由 diff 缓存的可用数据量决定。
decodeStep=diff->cache_end-diff->cache_begin;
if (decodeStep==0){ // 如果 diff 缓存为空,则填充。
_cache_update(diff);
decodeStep=diff->cache_end-diff->cache_begin;
}
}else{
// 无需差分:块大小可以任意定,这里巧妙地复用了 diff 缓存的总大小作为处理块的大小。
decodeStep=diff->cache_end;
}

_CHECK(decodeStep>0); // 确保处理块大小有效。
// 本次处理的块大小不能超过剩余需要的总长度。
if (decodeStep>addLength)
decodeStep=(hpi_size_t)addLength;

// 核心操作步骤:
// 1. 从旧文件读取数据块到 "工作台" temp_cache。
_CHECK(old_and_new->read_old(old_and_new,oldPos,temp_cache,(hpi_pos_t)decodeStep));

// 2. 如果需要差分运算...
if (!isNotNeedSubDiff){
// 将 diff 缓存中的数据加到 temp_cache 中。
addData(temp_cache,diff->cache_buf+diff->cache_begin,decodeStep);
// 消耗掉已使用的 diff 缓存数据。
diff->cache_begin+=decodeStep;
}

// 3. 将 "工作台" temp_cache 中的结果数据写入新文件。
_CHECK(old_and_new->write_new(old_and_new,temp_cache,decodeStep));

// 更新旧文件的读取位置和剩余处理长度。
oldPos+=decodeStep;
addLength-=decodeStep;
}
return hpi_TRUE;
}

_hpi_cache_success_finish Hpatch Lite 源码解析:流结束状态校验

原理与设计思路解析

这个函数 _hpi_cache_success_finish 是在整个补丁应用过程 hpatch_lite_patch 的最后一步被调用的,作为最终的校验条件之一。

  • 函数目的:最终健全性检查 (Final Sanity Check)

    • 它的作用不是检查补丁文件末尾是否还有多余的数据,而是对输入流缓存 (_TInputCache) 的最终状态进行一次简单的健全性检查。
    • 它被用于 return (newSize==newPosBack) & _cache_success_finish(&diff); 这一行,与“生成的新文件大小是否正确”这个条件共同决定了整个补丁操作是否成功。
  • self->cache_end != 0 的含义

    • 要理解这个判断,我们需要回顾 _TInputCache 的工作机制。cache_end 成员变量记录的是当前缓冲区中有效数据的末尾位置(或者说,有效数据的长度)。
    • 这个值只会在一个地方被设置为 0:在 _cache_update 函数中,当底层的 read_code 函数尝试从输入流读取数据,但失败了或者读到了文件末尾 (EOF),返回的读取长度为 0 时。
    • 因此,self->cache_end 变成 0 标志着输入流已经枯竭
    • 那么,return (self->cache_end != 0) 的逻辑就是:“只要输入流没有进入枯竭状态,就认为是成功的。”
  • 为什么这是有效的?

    • 在一个设计良好的补丁流程中,hpatch_lite_patch 函数的主循环会精确地读取所有它需要的数据。如果补丁文件是完整的,那么当主循环结束时,所有必需的数据都已经被消费掉了。
    • 此时,_TInputCache 的状态可能是:其内部缓冲区中还有一些从最后一次I/O读取中剩下的、但未使用的数据。在这种情况下,cache_end 显然不为 0
    • 如果补丁文件被意外截断,那么在主循环的某个时刻,_cache_read_1byte 会发现缓存为空并调用 _cache_update,而 _cache_update 会因为读不到数据而将 cache_end 置为 0,并返回失败。这个失败会通过 _CHECK 宏被捕获,导致 hpatch_lite_patch 提前失败退出。
    • 所以,这个最终的检查可以看作是一个“兜底”的校验,确保补丁流程正常结束后,输入流的状态依然是健康的(即没有在不该结束的时候结束)。
  • 性能优化 (hpi_force_inline)

    • 这个函数非常短小,就是一个简单的比较操作。hpi_force_inline 是一个给编译器的强烈建议(甚至在某些编译器上是强制指令),告诉它不要为这个函数生成常规的函数调用(这会涉及压栈、跳转等开销),而是直接将 return (self->cache_end!=0); 这行代码嵌入到调用它的地方。
    • 对于这种在核心流程末尾的、极度简单的函数,内联可以消除微小的性能开销。

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 为内部函数 _hpi_cache_success_finish 创建一个简短的别名。
// 这是一种常见的编码风格,用于隐藏实现细节并提供一个更清晰的API。
#define _cache_success_finish _hpi_cache_success_finish

/**
* @brief 对输入缓存的状态进行最终的健全性检查。
* @param self 指向 _TInputCache 实例的常量指针。
* @return hpi_BOOL 如果缓存状态被认为是成功的,则返回 hpi_TRUE。
*
* `static` 关键字意味着此函数仅在当前文件内可见。
* `hpi_force_inline` 是一个性能优化指令,建议编译器将此函数内联。
*/
static hpi_force_inline
hpi_BOOL _hpi_cache_success_finish(const _hpi_TInputCache* self){
// 返回一个布尔值,判断 cache_end 是否不为0。
// cache_end 只有在尝试从一个已结束或出错的流中读取数据时才会被置为0。
// 因此,这个检查确认了流在整个补丁过程中没有意外枯竭。
return (self->cache_end!=0);
}

hpatch_lite_patch Hpatch Lite 主补丁应用逻辑

原理与设计思路解析

hpatch_lite_patch 函数实现了基于一系列“覆盖(cover)”操作的补丁算法。它从补丁流中顺序读取指令,每一条指令都描述了如何生成新文件的一部分数据。

  • 核心概念:“覆盖(Cover)”与“新数据(New Data)”

    • Hpatch 算法将新文件看作是由两种数据组成的:

      1. 覆盖数据 (Cover Data): 这部分数据是通过将旧文件中的某一段数据,与补丁文件中的一小段“差分数据”相加(或直接复制)得到的。
      2. 新数据 (New Data): 这部分数据在旧文件中完全不存在,因此直接从补丁文件中复制而来。
    • 整个补丁过程就是交替进行“复制新数据”和“生成覆盖数据”的过程,直到新文件被完整创建。

  • 指令流与状态机

    • 函数首先从补丁流中读取一个 coverCount,它代表了总共有多少个“覆盖”操作。
    • 随后,函数进入一个大循环,每次循环处理一个“覆盖”操作。
    • 为了跟踪进度,函数维护了两个关键的状态变量(可以看作是“光标”):
      • newPosBack: 当前在新文件上已经写入到的位置。
      • oldPosBack: 上一个覆盖操作在旧文件上读取结束的位置。
  • 关键优化:差量编码 (Delta Encoding)

    • 为了让补丁文件尽可能小,cover_newPoscover_oldPos不是以绝对位置存储的,而是使用了差量编码
      • cover_newPos: 存储的是相对于上一个写入点 newPosBack 的增量。因为新文件总是向前生成的,所以 cover_newPos 总是大于等于 newPosBack
      • cover_oldPos: 存储的是相对于上一个读取点 oldPosBack 的偏移量,并且一个标志位决定了这个偏移是向前(+)还是向后(-)。这使得算法可以灵活地引用旧文件中的任何数据。
    • 这些经过差量编码后的小数值,可以被 _cache_unpackUInt 函数(即 VLQ/LEB128 编码)用非常少的字节来表示,这是 Hpatch 格式高效的关键。
  • 内存使用 (temp_cache)

    • 调用者传入一块内存 temp_cache,函数将其一分为二。
    • 后半部分用作 _TInputCache 的缓冲区,用于高效地从补丁数据流中读取指令和数据。
    • 前半部分则在 _patch_add_old_withClip 函数中被用作临时工作区,例如,用于存放从旧文件中读取的数据块,以便和差分数据进行运算。

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* @brief 执行 Hpatch Lite 的核心补丁逻辑。
* @param listener 一个包含所有I/O流句柄和回调函数的监听器结构体。
* @param newSize 期望生成的新文件的总大小(用于最后校验)。
* @param temp_cache 用于内部操作的临时内存缓冲区。
* @param temp_cache_size 临时缓冲区的大小。
* @return hpi_BOOL 成功返回 hpi_TRUE,失败返回 hpi_FALSE。
*/
hpi_BOOL hpatch_lite_patch(hpatchi_listener_t* listener,hpi_pos_t newSize,
hpi_byte* temp_cache,hpi_size_t temp_cache_size){
_TInputCache diff; // 创建一个输入缓存来读取 diff 文件。
hpi_pos_t coverCount; // 补丁中包含的“覆盖”操作的总数。
hpi_pos_t newPosBack=0; // “光标”,记录当前在新文件中的写入位置。
hpi_pos_t oldPosBack=0; // “光标”,记录上一次在旧文件中的读取位置。
{
// --- 初始化输入缓存 ---
assert(temp_cache); // 确保临时缓存有效。
_SAFE_CHECK(temp_cache_size>=hpi_kMinCacheSize); // 确保缓存大小满足最低要求。
temp_cache_size>>=1; // 将缓存大小除以2。

diff.cache_begin=temp_cache_size; // 缓存的起始和结束都指向中间,表示缓存为空。
diff.cache_end=temp_cache_size;
diff.cache_buf=temp_cache+temp_cache_size; // 使用传入缓存的后半部分作为 diff 读取缓冲区。
diff.inputStream=listener->diff_data; // 设置底层输入流为 diff 文件。
diff.read_code=listener->read_diff; // 设置读取函数。
}
// 从 diff 流中解包出“覆盖”操作的总数。
coverCount=_cache_unpackUInt(&diff,0,hpi_TRUE);

// 循环处理每一个“覆盖”操作。
while (coverCount--){
hpi_pos_t cover_oldPos; // 本次覆盖在旧文件中的起始位置。
hpi_pos_t cover_newPos; // 本次覆盖在新文件中的起始位置。
hpi_pos_t cover_length; // 本次覆盖的数据长度。
hpi_BOOL isNotNeedSubDiff; // 一个标志,表示是直接复制旧数据,还是需要和差分数据相加。

{// 从 diff 流中读取并解码一个覆盖操作的元数据
cover_length=_cache_unpackUInt(&diff,0,hpi_TRUE);
{
// 读取一个8位的标签(tag)字节,它包含了多种打包信息。
hpi_fast_uint8 tag=_cache_read_1byte(&diff);
// 解码 cover_oldPos 的差量值。
cover_oldPos=_cache_unpackUInt(&diff,tag&31,tag&(1<<5)); // 低5位是数值,第6位是连续标志。
isNotNeedSubDiff=(tag>>7); // 最高位是 isNotNeedSubDiff 标志。

// 根据第7位标志,决定 oldPos 是向前还是向后偏移。
if (tag&(1<<6))
cover_oldPos=oldPosBack-cover_oldPos; // 向后偏移
else
cover_oldPos+=oldPosBack; // 向前偏移
}
// 解码 cover_newPos 的差量值。
cover_newPos=_cache_unpackUInt(&diff,0,hpi_TRUE);
cover_newPos+=newPosBack; // newPos 总是向前累加。
}

// 校验数据合法性,新位置不能在已写位置之前。
_SAFE_CHECK(cover_newPos>=newPosBack);

// 如果新位置和上次写入位置之间有“沟”,说明这段是新数据。
if (newPosBack<cover_newPos)
// 直接从 diff 流中复制这段新数据到输出。
_CHECK(_patch_copy_diff(listener,&diff,cover_newPos-newPosBack));

// 执行核心的“覆盖”操作:从旧文件读取、与差分数据运算、写入新文件。
_CHECK(_patch_add_old_withClip(listener,&diff,isNotNeedSubDiff,cover_oldPos,cover_length,temp_cache));

// --- 更新状态光标,为下一次循环做准备 ---
newPosBack=cover_newPos+cover_length;
oldPosBack=cover_oldPos+cover_length;

// 校验:覆盖长度必须大于0,除非这是最后一个覆盖操作。
_SAFE_CHECK((cover_length>0)|(coverCount==0));
}

// 最终校验:
// 1. 生成的新文件大小是否和头部声明的一致。
// 2. diff 流是否被完整、正确地读取完毕。
/*
#define _cache_success_finish _hpi_cache_success_finish
static hpi_force_inline
hpi_BOOL _hpi_cache_success_finish(const _hpi_TInputCache* self){ return (self->cache_end!=0); }
*/
return (newSize==newPosBack)&_cache_success_finish(&diff);
}

hpatchi_inplace_open Hpatch In-place 头部数据解析

原理与设计思路解析

hpatchi_inplace_open 函数是 In-place 更新模式的入口,其核心职责与 hpatch_lite_open 类似:验证并解析补丁文件的头部。但它专门用于解析为 In-place 模式设计的、包含额外信息的头部。

  • 文件头结构 (In-place Header Format)
    In-place 模式的头部是 lite 模式头部的扩展,增加了一个关键字段。其头部大小 hpi_kInplaceHeadSize 通常为 5 字节
字节偏移 字段名称 长度 (字节) 描述
0 Magic Byte 1 1 固定为 'h',用于文件类型识别。
1 Magic Byte 2 1 固定为 'I',与'h'共同组成 “hI” 魔法标识。
2 compress_type 1 压缩类型。标识补丁数据使用了哪种压缩算法(如 zlib, bzip2, lzma 等)。
3 Packed Info 1 一个被打包的字节,通过位运算存储了3种不同的信息(同 lite 模式)。
4 extraSafeSize len 1 新增字节:表示 extraSafeSize 字段本身占用了多少个字节。
  • Packed Info 字节 (第3字节) 的详细解析 (与 lite 模式相同):

    • 高2位 (Bits 6-7): (byte >> 6)
      • 版本号 (Version Code): 值为 kHPatchLite_inplaceCode (2),用于与 lite 模式区分。
    • 中3位 (Bits 3-5): ((byte >> 3) & 7)
      • uncompressSize 的字节数: 存储该字段需要读取的字节数。
    • 低3位 (Bits 0-2): (byte & 7)
      • newSize 的字节数: 存储该字段需要读取的字节数。
  • 新增的核心字段:extraSafeSize

    • 这是 In-place 模式与 lite 模式最根本的区别。
    • extraSafeSize 是由补丁生成工具 (hpatch_diff) 在分析完整个差分过程后计算出的一个安全值。它代表了在整个原地更新过程中,为了避免“读写冲突”(即写入新数据的位置覆盖了稍后需要读取的旧数据),必须设置的最小内存缓冲区大小
    • 解析流程与 newSize 类似,也是一个两步过程:
      1. 先从第4个字节读取 extraSafeSize 这个数值本身占用了多少字节(长度)。
      2. 再根据这个长度,从流中读取相应字节数的数据,并用 _hpi_readSize 将其转换为一个整数。
  • 数据读取流程

    1. 读取固定头部: 首先,函数读取固定长度(hpi_kInplaceHeadSize,即5字节)的头部数据。
    2. 校验与解析:
      • 校验文件魔法标识 'hI' 和 In-place 版本号,确保文件类型正确。
      • 直接读取压缩类型。
      • 通过位运算从第3字节中解包newSizeuncompressSize 这两个字段的长度
      • 直接从第4字节读取 extraSafeSize 字段的长度
    3. 读取可变长数据: 根据上一步解析出的三个长度值,依次从数据流中分别读取 newSizeuncompressSizeextraSafeSize 的实际数据。
    4. 字节到整数的转换: 调用 _hpi_readSize 将读取到的字节数据转换为整数,并填充到对应的输出参数中。

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* @brief 打开并解析一个 Hpatch In-place 格式的补丁流,读取其头部信息。
* @param diff_data 补丁数据流的句柄。
* @param read_diff 用于从流中读取数据的函数指针。
* @param out_compress_type 输出参数,用于返回解析出的压缩类型。
* @param out_newSize 输出参数,用于返回补丁应用后的新文件大小。
* @param out_uncompressSize 输出参数,用于返回解压缩后数据的大小。
* @param out_extraSafeSize 输出参数,用于返回原地更新所需的安全缓冲区大小。
* @return hpi_BOOL 成功返回 hpi_TRUE,失败返回 hpi_FALSE。
*/
hpi_BOOL hpatchi_inplace_open(hpi_TInputStreamHandle diff_data,hpi_TInputStream_read read_diff,
hpi_compressType* out_compress_type,hpi_pos_t* out_newSize,
hpi_pos_t* out_uncompressSize,hpi_size_t* out_extraSafeSize){
// 定义 In-place 模式的版本号常量。
#define kHPatchLite_inplaceCode 2

// lenn, lenu, lene 用于临时存储各个字段的长度信息。
hpi_size_t lenn=hpi_kInplaceHeadSize; // 期望读取的头部长度(通常为5字节)。
hpi_size_t lenu,lene;

// 定义一个缓冲区,确保其大小足以容纳固定头部或任何一个可变长度的整数。
hpi_byte buf[(hpi_kInplaceHeadSize>sizeof(hpi_pos_t))?hpi_kInplaceHeadSize:sizeof(hpi_pos_t)];

// 通过函数指针 read_diff 读取固定长度的头部到 buf 中。
_CHECK(read_diff(diff_data,buf,&lenn));

// --- 开始校验和解析头部 ---

// buf[3] 是包含版本号和两个长度信息的 "Packed Info" 字节。
lenu=buf[3];

// _SAFE_CHECK 宏用于验证头部数据的合法性。
// 1. lenn==hpi_kInplaceHeadSize: 确认是否成功读取了预期长度的头部。
// 2. (buf[0]=='h')&(buf[1]=='I'): 校验文件魔法标识是否为 "hI"。
// 3. ((lenu>>6)==kHPatchLite_inplaceCode): 将 buf[3] 右移6位取出高2位,校验版本号是否为 In-place 的版本号(2)。
_SAFE_CHECK((lenn==hpi_kInplaceHeadSize)&(buf[0]=='h')&(buf[1]=='I')&((lenu>>6)==kHPatchLite_inplaceCode));

// buf[2] 直接存储了压缩类型。
*out_compress_type=buf[2];

// 从 "Packed Info" 字节 (buf[3]) 中解包长度信息。
lenn=lenu&7; // 取低3位,得到 newSize 字段的字节数。
lenu=(lenu>>3)&7; // 取中3位,得到 uncompressSize 字段的字节数。

// buf[4] 直接存储了 extraSafeSize 字段的字节数。
lene=buf[4];

// 校验解析出的三个长度是否合法(不能超过其对应类型的最大字节数)。
_SAFE_CHECK((lenn<=sizeof(hpi_pos_t))&(lenu<=sizeof(hpi_pos_t))&(lene<=sizeof(hpi_size_t)));

// --- 读取可变长度的元数据 ---

// 根据长度 lenn,读取 newSize 的数据。
_CHECK(read_diff(diff_data,buf,&lenn));
// 调用 _hpi_readSize 将字节数据转换为整数。
*out_newSize=_hpi_readSize(buf,lenn);

// 根据长度 lenu,读取 uncompressSize 的数据。
_CHECK(read_diff(diff_data,buf,&lenu));
*out_uncompressSize=_hpi_readSize(buf,lenu);

// 根据长度 lene,读取 extraSafeSize 的数据。
_CHECK(read_diff(diff_data,buf,&lene));
*out_extraSafeSize=(hpi_size_t)_hpi_readSize(buf,lene);

// 所有操作成功,返回真。
return hpi_TRUE;
}

Hpatch In-place I/O Interception and Buffering

  • 这部分代码是实现“写延迟”以规避读写冲突的关键,它通过一个精巧的环形缓冲区(Ring Buffer)和可选的页缓冲机制来完成。

原理与设计思路解析

这组函数共同构成了一个I/O装饰器中间件 (hpatchi_listener_extra_t)。它的核心任务是拦截所有对新文件的写操作,将它们暂存到一个安全的内存缓冲区中,直到可以安全地将这些数据写入物理文件时,再执行实际的I/O。

  • 核心机制:环形缓冲区 (Ring Buffer)

    • _hpatchi_listener_extra_write_new_hpatchi_listener_extra_out 共同实现了一个经典的先进先出 (FIFO) 环形缓冲区。这块缓冲区就是 write_extra 内存,大小为 kWriteExtraSize
    • cached_data_pos 是缓冲区的读指针(下一个要被写出到文件的字节位置)。
    • cached_data_pos + cached_data_len(需要考虑回绕)是缓冲区的写指针(下一个新数据要写入的位置)。
    • _hpatchi_listener_extra_write_new (生产者):hpatch_lite_patch 引擎需要写入新数据时,此函数被调用。它将数据添加到环形缓冲区的末尾(写指针处)。如果添加新数据会导致缓冲区溢出,它会先调用 _hpatchi_listener_extra_out 来清空缓冲区最前端的旧数据,以腾出空间。
    • _hpatchi_listener_extra_out (消费者): 当需要从缓冲区刷写数据到文件时,此函数被调用。它从环形缓冲区的头部(读指针处)读取数据,并通过底层的 _wrap_listener 将其真正写入文件。

    环形缓冲区工作示意图:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        write_extra buffer:
    +-------------------------------------------------------------+
    | |##########| (cached_data_len) | |
    +-------------------------------------------------------------+
    ^ ^
    | |
    cached_data_pos 缓存数据结束
    (从这里读取) (在这里写入)

    // 当pos+len超过末尾时,会回绕到开头
    +-------------------------------------------------------------+
    |#####| (len) | |##########| (len) |
    +-------------------------------------------------------------+
    ^ ^
    | |
    End of cached data cached_data_pos
  • 可选的二级缓冲:页缓冲 (_IS_WTITE_NEW_BY_PAGE)

    • 目的: 许多存储介质(特别是 NAND Flash)以“页 (Page)”为单位进行写入时效率最高。频繁的小数据量写入会导致性能下降和“写放大”问题。这个可选特性就是为了优化这种情况。
    • 机制: 当这个特性被启用时,_hpatchi_listener_extra_out 的行为会改变。它不再直接将数据写入文件,而是将数据传递给第二个缓冲层——_hpatchi_listener_write_page
    • _hpatchi_listener_write_page 会将接收到的数据积累在一个专门的页缓冲区里 (write_extra + kWriteExtraSize)。只有当这个页缓冲区被写满时,_hpatchi_listener_page_out 才会被调用,将一整个满页的数据一次性写入物理文件。
    • 这形成了一个两级缓冲流水线:hpatch 引擎 -> extraSafeSize环形缓冲 -> pageSize页缓冲 -> 物理文件
  • 收尾工作 (_hpatchi_listener_extra_flush)

    • 在整个补丁过程结束后,extraSafeSize 环形缓冲和 pageSize 页缓冲中几乎肯定还留有未被写出的数据。
    • _hpatchi_listener_extra_flush 的职责就是在最后时刻被调用,确保这两个缓冲区中的所有剩余数据都被强制刷写到物理文件中,保证新文件的完整性。

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#if (_IS_WTITE_NEW_BY_PAGE!=0)
/**
* @brief (页模式) 将一个满页的数据刷写到底层文件流。
*/
static hpi_BOOL _hpatchi_listener_page_out(hpatchi_listener_extra_t* self){
// 调用被包装的 listener,将页缓冲区的数据真正写入文件。
// 页缓冲区位于 extraSafeSize 缓冲区的正后方。
hpi_BOOL result=self->_wrap_listener->write_new(self->_wrap_listener,
self->write_extra+self->kWriteExtraSize,self->page_data_len);
self->page_data_len=0; // 写出后,清空页缓冲区。
return result;
}

/**
* @brief (页模式) 将数据写入页缓冲区,并在页满时自动刷写。
* 这是一个二级缓冲,用于凑齐整页数据再进行I/O。
*/
static hpi_force_inline
hpi_BOOL _hpatchi_listener_write_page(hpatchi_listener_extra_t* self,const hpi_byte* data,hpi_size_t data_size){
hpi_byte* page_buf=self->write_extra+self->kWriteExtraSize;
while (data_size){
// 计算当前页还剩多少空间。
hpi_size_t cur_len=self->kPageSize-self->page_data_len;
if (cur_len==0){ // 如果页满了...
if (!_hpatchi_listener_page_out(self)) return _hpi_FALSE; // 刷写当前页。
cur_len=self->kPageSize; // 重置可用空间。
}
// 确定本次要复制的长度(不能超过剩余空间或输入数据长度)。
cur_len=(cur_len<=data_size)?cur_len:data_size;
data_size-=cur_len;
// 将数据逐字节复制到页缓冲区。
while (cur_len--)
page_buf[self->page_data_len++]=*data++;
}
return hpi_TRUE;
}
#endif //_IS_WTITE_NEW_BY_PAGE

/**
* @brief 从 extraSafeSize 环形缓冲区的头部,消费(写出)指定长度的数据。
*/
static hpi_BOOL _hpatchi_listener_extra_out(hpatchi_listener_extra_t* self,hpi_size_t out_size){
assert(out_size<=self->cached_data_len); // 确保请求写出的数据量不超过已缓存的数据量。
while (out_size){
// 计算从当前读指针(cached_data_pos)到缓冲区物理末尾的连续数据长度。
hpi_size_t cur_len=self->kWriteExtraSize-self->cached_data_pos;
cur_len=(cur_len<=out_size)?cur_len:out_size;

#if (_IS_WTITE_NEW_BY_PAGE!=0)
// 页模式:将数据发送到二级页缓冲。
if (!_hpatchi_listener_write_page(self,self->write_extra+self->cached_data_pos,cur_len))
return _hpi_FALSE;
#else
// 标准模式:直接将数据写入底层文件流。
if (!self->_wrap_listener->write_new(self->_wrap_listener,self->write_extra+self->cached_data_pos,cur_len))
return _hpi_FALSE;
#endif //_IS_WTITE_NEW_BY_PAGE

// 更新环形缓冲区的读指针和剩余数据长度。
self->cached_data_pos+=cur_len;
// 实现环形回绕:如果读指针到达末尾,则归零。
self->cached_data_pos-=(self->cached_data_pos<self->kWriteExtraSize)?0:self->kWriteExtraSize;
self->cached_data_len-=cur_len;
out_size-=cur_len;
}
return hpi_TRUE;
}

/**
* @brief 在补丁过程结束后,刷写所有缓冲区中剩余的数据。
*/
static hpi_force_inline
hpi_BOOL _hpatchi_listener_extra_flush(hpatchi_listener_extra_t* self){
// 首先,刷写 extraSafeSize 环形缓冲区中所有剩余的数据。
hpi_BOOL result=_hpatchi_listener_extra_out(self,self->cached_data_len);
#if (_IS_WTITE_NEW_BY_PAGE!=0)
// 页模式下,如果环形缓冲刷写成功,且页缓冲中还有数据,则强制刷写最后一页(即使不满)。
if (result&&(self->page_data_len))
result=_hpatchi_listener_page_out(self);
#endif //_IS_WTITE_NEW_BY_PAGE
return result;
}

/**
* @brief 拦截对新文件的写操作,将数据存入 extraSafeSize 环形缓冲区。
* 这是被 hpatch_lite_patch 调用的核心“写”函数。
*/
static hpi_BOOL _hpatchi_listener_extra_write_new(hpatchi_listener_t* listener,const hpi_byte* data,hpi_size_t data_size){
hpatchi_listener_extra_t* self=(hpatchi_listener_extra_t*)listener;
while (data_size){
hpi_size_t posi;
// 计算如果把新数据加进来,总共需要多少缓存。
hpi_size_t cur_len=data_size+self->cached_data_len;
// 如果需要的缓存大于总容量...
if (cur_len>self->kWriteExtraSize){
// ...就需要从缓冲区头部腾出空间。
cur_len-=self->kWriteExtraSize; // 计算需要腾出的最小空间。
// 确保腾出的空间不超过当前已缓存的数据量。
cur_len=(cur_len<=self->cached_data_len)?cur_len:self->cached_data_len;
// 调用 out 函数将最旧的数据写出。
if (!_hpatchi_listener_extra_out(self,cur_len)) return _hpi_FALSE;
}
assert(self->cached_data_len<self->kWriteExtraSize);

// 计算当前可写入的连续空间大小。
cur_len=self->kWriteExtraSize-self->cached_data_len;
cur_len=(cur_len<=data_size)?cur_len:data_size;
data_size-=cur_len;

// 计算写入的起始物理位置(写指针)。
posi=self->cached_data_pos+self->cached_data_len;
self->cached_data_len+=cur_len;

// 将新数据逐字节复制到环形缓冲区。
while (cur_len--){
// 实现环形回绕。
posi-=(posi<self->kWriteExtraSize)?0:self->kWriteExtraSize;
self->write_extra[posi++]=*data++;
}
}
return hpi_TRUE;
}

hpatchi_inplaceB Hpatch In-place 补丁协调器

原理与设计思路解析

hpatchi_inplaceB 的核心职责不是执行补丁算法本身,而是创建一个特殊的环境,然后在这个环境中调用通用的 hpatch_lite_patch 函数来完成实际工作。这个“特殊环境”的关键就是我们之前讨论过的 hpatchi_listener_extra_t 包装器。

  • 核心策略:内存分区与I/O重定向
    这个函数最精妙的设计在于它对调用者提供的单一内存块 temp_cache 的巧妙划分和使用。它将这块内存“一分为二”:

    1. 前端部分 (Extra Buffers): 专门用于 In-place 模式的特殊需求,即 extraSafeSize 安全缓冲和可选的 pageSize 页缓冲。
    2. 后端部分 (Standard Cache): 剩下的内存则被原封不动地传递给 hpatch_lite_patch,供其内部用于标准的差分流读取缓存 (_TInputCache) 和数据运算。

    内存布局示意图:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <------------------------------- temp_cache_size --------------------------------->
    +--------------------------------+-----------------------------------------------+
    | 前端 (Extra) | 后端 (Standard) |
    +================================+===============================================+
    | extraSafeSize | (opt) pageSize | 传递给 hpatch_lite_patch 供其自身使用 |
    | (write_extra | (page_buf for | (例如,不同的流读取缓存,临时工作区)|
    | ring buffer) | page writes) | |
    +--------------------------------+-----------------------------------------------+
    ^ ^
    | |
    temp_cache temp_cache + needExtra
  • 设计模式:装饰器与逻辑复用
    该函数完美体现了“装饰器模式”和代码复用思想。

    • 创建并初始化 extra_listener,这个 extra_listener 装饰 (或包装) 了原始的 listener
    • 然后,它复用了完全通用的 hpatch_lite_patch 算法。补丁引擎本身无需关心是否在进行 In-place 更新,它只管通过 listener 接口读写数据。
    • 由于传递进去的是被装饰过的 extra_listener,所有 write_new 操作都被透明地拦截并路由到我们的“写延迟”环形缓冲逻辑中,从而实现了原地更新的安全性。
  • 编译时配置 (_IS_WTITE_NEW_BY_PAGE)
    代码使用了C预处理器 #if 来生成两个版本的函数:

    • hpatchi_inplaceB_by_page: 当需要按页对齐写入时编译,接收一个 pageSize 参数。
    • hpatchi_inplaceB: 标准版本,内部将 pageSize 视为0。
      这种方式使得核心逻辑可以共享,同时为特定需求(如操作Flash存储)提供专门的接口。
  • 一个精巧的健壮性设计
    代码中有一行 extraSafeSize=(extraSafeSize>=cache_size)?extraSafeSize:cache_size;。这是一个非常巧妙的健壮性保证。它确保了 extraSafeSize 安全缓冲区的大小至少不小于后面 hpatch_lite_patch 将要使用的差分流读取缓存 (cache_size) 的大小。这可以防止因 extraSafeSize 过小而 hpatch_lite_patch 的读缓存很大时,可能引发的性能问题或逻辑冲突,保证了系统的稳定运行。

代码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#if (_IS_WTITE_NEW_BY_PAGE!=0)
// 当需要按页写入时,编译此函数签名。
hpi_BOOL hpatchi_inplaceB_by_page(hpatchi_listener_t* listener,hpi_pos_t newSize,hpi_byte* temp_cache,
hpi_size_t extraSafeSize,hpi_size_t pageSize,hpi_size_t temp_cache_size){
#else //_IS_WTITE_NEW_BY_PAGE==0
// 否则,编译标准版函数签名。
hpi_BOOL hpatchi_inplaceB(hpatchi_listener_t* listener,hpi_pos_t newSize,
hpi_byte* temp_cache,hpi_size_t extraSafeSize,hpi_size_t temp_cache_size){
// 在标准版中,pageSize 逻辑上为0,使其在后续计算中无效。
const hpi_size_t pageSize=0;
#endif //_IS_WTITE_NEW_BY_PAGE
hpi_BOOL result;
hpatchi_listener_extra_t extra_listener; // 栈上分配装饰器实例。

// 计算前端“特殊缓冲”需要的总内存。
const hpi_size_t needExtra=extraSafeSize+pageSize;

// 断言检查:确保调用者提供的总内存足够大。
assert(temp_cache_size>=hpi_kMinInplaceCacheSize+extraSafeSize+pageSize);
// _hpi_Do4Page 宏包裹的代码只在“按页写入”版本中生效。
_hpi_Do4Page(assert(pageSize>0));

// 一个健壮性检查和优化。
if (needExtra){
// 计算扣除特殊缓冲后,留给 hpatch_lite_patch 的内存,并将其一半用作其内部的读缓存大小。
hpi_size_t cache_size=(temp_cache_size-needExtra)>>1;
// 确保 extraSafeSize 至少不小于 hpatch_lite_patch 的读缓存大小。
extraSafeSize=(extraSafeSize>=cache_size)?extraSafeSize:cache_size;
}

// 初始化装饰器。它将拦截 listener 的写操作,并使用 temp_cache 的起始部分作为环形缓冲区。
_hpatchi_listener_extra_init(&extra_listener,listener,temp_cache,extraSafeSize);

// _hpi_Do4Page 宏确保以下代码只在“按页写入”版本中执行。
_hpi_Do4Page(extra_listener.kPageSize=pageSize); // 设置页大小
_hpi_Do4Page(extra_listener.page_data_len=0); // 初始化页数据长度
_hpi_Do4Page(extraSafeSize+=pageSize); // 更新偏移量,为页缓冲区腾出空间

// 调用通用的补丁函数,这是整个设计的核心!
result=hpatch_lite_patch(
needExtra?&extra_listener.base:listener, // 如果需要特殊缓冲,就传入装饰器;否则直接传入原始listener。
newSize,
temp_cache+extraSafeSize, // 巧妙地将 temp_cache 的“后端部分”内存传递给 hpatch_lite_patch。
temp_cache_size-extraSafeSize // 传递后端内存的相应大小。
);

// 最终结果:必须补丁过程成功,并且最终的缓存刷写操作也成功。
return result&&_hpatchi_listener_extra_flush(&extra_listener);
}