逆向 | LZSS压缩算法分析

LZSS压缩算法分析

在galgame汉化学习的过程中碰到的lzss算法,然而网上似乎找不到一个能讲明白的文章,很是恼火。
所幸有一些可以跑的代码,就通过代码分析来学习一下了。

主要参考:
https://blog.csdn.net/qq_34254642/article/details/103651815
https://blog.csdn.net/qq_34254642/article/details/103741228

下面的代码我贴上了详细的注释,配上参考文章中第一篇写的理论知识应该不难懂了。
关键点在于滑动窗口、向前缓冲区作用的理解。
当然我写的也不怎么好,但是我自己已经能看懂了。
原文的python代码中用了一些意义不明的位运算,我直接简化了一下。
有一点点细节的地方还是不太明白,有空再研究。

import ctypes
import os

class LZSS():
	def __init__(self, preBufSizeBits):
		self.threshold = 2  #长度大于等于2的匹配串才有必要压缩
		# 前向缓冲区占用的比特位
		self.preBufSizeBits = preBufSizeBits 
		# 滑动窗口占用的比特位, 等于说2字节扣掉向前缓冲区的bit剩下来的
		self.windowBufSizeBits = 16 - self.preBufSizeBits   
		print(f'preBufSizeBits={self.preBufSizeBits}')
		print(f'windowBufSizeBits={self.windowBufSizeBits}')

		# 通过占用的比特位计算缓冲区大小 2^(preBufSizeBits) - 1 + 2
		self.preBufSize = 2 ** self.preBufSizeBits - 1 + self.threshold 
		# 通过占用的比特位计算滑动窗口大小
		self.windowBufSize = 2 ** self.windowBufSizeBits - 1 + self.threshold   
		print(f'preBufSize={self.preBufSize}')
		print(f'windowBufSize={self.windowBufSize}')

		self.preBuf = b''   #前向缓冲区
		self.windowBuf = b''    #滑动窗口
		self.matchString = b''  #匹配串
		self.matchIndex = 0     #滑动窗口匹配串起始下标

	#文件压缩
	def LZSS_encode(self, readfilename, writefilename):

		fread = open(readfilename, "rb")
		fwrite = open(writefilename, "wb")
		restorebuff = b''   #待写入的数据缓存区,满一组数据写入一次文件
		itemnum = 0     # 8个项目为一组,用来统计当前项目数
		signbits = 0    # 标记字节, 用于标记后面的8个项目哪个是压缩的,哪个是原文

		# 读取数据填满前向缓冲区(先读一管子内容)
		self.preBuf = fread.read(self.preBufSize)    # 读取129个字节的内容 
		print('读取缓冲区:',end='')
		print(self.preBuf)

		# 前向缓冲区没数据可操作了即为压缩结束
		while self.preBuf != b'':
			# 初始化匹配串和匹配位置
			self.matchString = b''
			self.matchIndex = -1
			
			print(f'  > 开始查找匹配串,当前滑动窗口为:{self.windowBuf}')
			#在滑动窗口中寻找 最长 的匹配串
			for i in range(self.threshold, len(self.preBuf) + 1):  # 从第二位开始查找,直到最后一位
				index = self.windowBuf.find(self.preBuf[0:i])   # 在滑动窗口中查找匹配的字符串,滑动窗口一开始为空
				
				print(f'    > 查找: {self.preBuf[0:i]}')
				if index != -1:   # 匹配到,记录一下匹配串和匹配位置,然后循环,找最长的匹配串
					print(f'    > 发现匹配串: {self.preBuf[0:i]}, 位置: {index}')
					self.matchString = self.preBuf[0:i]
					self.matchIndex = index
				else:     # 如果前两个字符都没有匹配的,就直接跳出循环了
					break
			print('  > 查找完毕\n')
					
			#如果没找到匹配串或者匹配长度为1,直接输出原始数据
			if self.matchIndex == -1: # 没匹配上
				self.matchString = self.preBuf[0:1]  # 存一个字节
				restorebuff += self.matchString
			else:    # 匹配上了
				print(f'> 匹配上的编号:{(self.matchIndex << self.preBufSizeBits) % 2**16 + len(self.matchString) - self.threshold}\n  -{self.matchIndex * (2 ** self.preBufSizeBits) + len(self.matchString) - self.threshold}\n  -{ctypes.c_uint16(self.matchIndex * (2 ** self.preBufSizeBits) + len(self.matchString) - self.threshold)} \n')
				restorebuff += bytes(ctypes.c_uint16(self.matchIndex * (2 ** self.preBufSizeBits) + len(self.matchString) - self.threshold))
				# 这句话比较难以理解,实际上就是修改标志位,标志位就是一个字节,用于标识后面的字节有没有被压缩
				signbits += (2 ** (7 - itemnum))
			
			
			#操作完一个项目+1
			itemnum += 1
			#项目数达到8了,说明做完了一组压缩,将这一组数据写入文件
			if itemnum >= 8:
				print('> 一组数据压缩完成')
				print(f'> signbits: {signbits} - {bytes(ctypes.c_uint8(signbits))} - {bin(signbits)}')
				print(f'> restorebuff: {restorebuff}')
				writebytes = bytes(ctypes.c_uint8(signbits)) + restorebuff
				print(f'> 真实存储:{writebytes}\n\n')
				fwrite.write(writebytes);
				itemnum = 0       # 清空项目数量、标志位、存储的字节
				signbits = 0
				restorebuff = b''

			
			self.preBuf = self.preBuf[len(self.matchString):]  #将刚刚匹配过的数据移出前向缓冲区
			# 将刚刚匹配过的数据加入滑动窗口
			# 如果啥也没匹配上,就相当于从向前缓冲区开头拿一个字节放到滑动窗口的结尾
			self.windowBuf += self.matchString  
			
			# 移动滑动窗口
			if len(self.windowBuf) > self.windowBufSize:  #将多出的数据从前面开始移出滑动窗口
				self.windowBuf = self.windowBuf[(len(self.windowBuf) - self.windowBufSize):]

			# 填充一下向前缓冲区
			self.preBuf += fread.read(self.preBufSize - len(self.preBuf))  #读取数据补充前向缓冲区

		
		if restorebuff != b'':  #文件最后可能不满一组数据量,直接写到文件里
			writebytes = bytes(ctypes.c_uint8(signbits)) + restorebuff
			print(f'> 最后一组真实存储:{writebytes}\n\n')
			fwrite.write(writebytes)

		fread.close()
		fwrite.close()

		return os.path.getsize(writefilename)

	#文件解压
	def LZSS_decode(self, readfilename, writefilename):
		fread = open(readfilename, "rb")
		fwrite = open(writefilename, "wb")

		self.windowBuf = b''
		self.preBuf = fread.read(1)  #先读一个标记字节以确定接下来怎么解压数据

		while self.preBuf != b'':
			for i in range(8):  #8个项目为一组进行解压
				# 从标记字节的最高位开始解析,0代表原始数据,1代表(下标,匹配数)解析
				if self.preBuf[0] & (1 << (7 - i)) == 0:
					temp = fread.read(1)
					fwrite.write(temp)
					self.windowBuf += temp
				else:
					temp = fread.read(2)
					start = ((temp[0] + temp[1] * 256) // (1 << self.preBufSizeBits))  #取出高位的滑动窗口匹配串下标
					end = start + temp[0] % (1 << self.preBufSizeBits) + self.threshold  #取出低位的匹配长度
					fwrite.write(self.windowBuf[start:end])  #将解压出的数据写入文件
					self.windowBuf += self.windowBuf[start:end]  #将解压处的数据同步写入到滑动窗口

				if len(self.windowBuf) > self.windowBufSize:  #限制滑动窗口大小
					self.windowBuf = self.windowBuf[(len(self.windowBuf) - self.windowBufSize):]

			self.preBuf = fread.read(1)  #读取下一组数据的标志字节

		fread.close()
		fwrite.close()

if __name__ == '__main__':
	Demo = LZSS(7)
	Demo.LZSS_encode("origin.txt", "encode")
	Demo.LZSS_decode("encode", "decode.txt")

再留档一个c语言的版本:

#include <string.h>
#include <stdio.h>
 
#define BYTE unsigned char
#define WORD unsigned short
#define DWORD unsigned int
 
#define TRUE 1
#define FALSE 0
 
BYTE bThreshold;  //压缩阈值、长度大于等于2的匹配串才有必要压缩
 
BYTE bPreBufSizeBits;  //前向缓冲区占用的比特位
BYTE bWindowBufSizeBits;  //滑动窗口占用的比特位
 
WORD wPreBufSize;  //通过占用的比特位计算缓冲区大小
WORD wWindowBufSize;  //通过占用的比特位计算滑动窗口大小
 
BYTE bPreBuf[1024];  //前向缓冲区
BYTE bWindowBuf[8192];  //滑动窗口
BYTE bMatchString[1024];  //匹配串
WORD wMatchIndex;  //滑动窗口匹配串起始下标
 
BYTE FindSameString(BYTE *pbStrA, WORD wLenA, BYTE *pbStrB, WORD wLenB, WORD *pwMatchIndex);  //查找匹配串
DWORD LZSS_encode(char *pbReadFileName, char *pbWriteFileName);  //文件压缩
DWORD LZSS_decode(char *pbReadFileName, char *pbWriteFileName);  //文件解压
 
int main()
{
	bThreshold = 2;
	bPreBufSizeBits = 7;
	bWindowBufSizeBits = 16 - bPreBufSizeBits;
	wPreBufSize = ((WORD)1 << bPreBufSizeBits) - 1 + bThreshold;
	wWindowBufSize = ((WORD)1 << bWindowBufSizeBits) - 1 + bThreshold;
 
	LZSS_encode("origin.txt", "encode");
	LZSS_decode("encode", "decode.txt");
	return 0;
}
 
BYTE FindSameString(BYTE *pbStrA, WORD wLenA, BYTE *pbStrB, WORD wLenB, WORD *pwMatchIndex)
{
	WORD i, j;
 
	for (i = 0; i < wLenA; i++)
	{
		if ((wLenA - i) < wLenB)
		{
			return FALSE;
		}
 
		if (pbStrA[i] == pbStrB[0])
		{
			for (j = 1; j < wLenB; j++)
			{
				if (pbStrA[i + j] != pbStrB[j])
				{
					break;
				}
			}
 
			if (j == wLenB)
			{
				*pwMatchIndex = i;
				return TRUE;
			}
		}
	}
	return FALSE;
}
 
DWORD LZSS_encode(char *pbReadFileName, char *pbWriteFileName)
{
	WORD i, j;
	WORD wPreBufCnt = 0;
	WORD wWindowBufCnt = 0;
	WORD wMatchStringCnt = 0;
	BYTE bRestoreBuf[17] = { 0 };
	BYTE bRestoreBufCnt = 1;
	BYTE bItemNum = 0;
	FILE *pfRead = fopen(pbReadFileName, "rb");
	FILE *pfWrite = fopen(pbWriteFileName, "wb");
 
	//前向缓冲区没数据可操作了即为压缩结束
	while (wPreBufCnt += fread(&bPreBuf[wPreBufCnt], 1, wPreBufSize - wPreBufCnt, pfRead))
	{
		wMatchStringCnt = 0;  //刚开始没有匹配到数据
		wMatchIndex = 0xFFFF;  //初始化一个最大值,表示没匹配到
 
		for (i = bThreshold; i <= wPreBufCnt; i++)  //在滑动窗口中寻找最长的匹配串
		{
			if (TRUE == FindSameString(bWindowBuf, wWindowBufCnt, bPreBuf, i, &wMatchIndex))
			{
				memcpy(bMatchString, &bWindowBuf[wMatchIndex], i);
				wMatchStringCnt = i;
			}
			else
			{
				break;
			}
		}
 
		//如果没找到匹配串或者匹配长度为1,直接输出原始数据
		if ((0xFFFF == wMatchIndex))
		{
			wMatchStringCnt = 1;
			bMatchString[0] = bPreBuf[0];
			bRestoreBuf[bRestoreBufCnt++] = bPreBuf[0];
		}
		else
		{
			j = (wMatchIndex << bPreBufSizeBits) + wMatchStringCnt - bThreshold;
			bRestoreBuf[bRestoreBufCnt++] = (BYTE)j;
			bRestoreBuf[bRestoreBufCnt++] = (BYTE)(j >> 8);
			bRestoreBuf[0] |= (BYTE)1 << (7 - bItemNum);
		}
 
		bItemNum += 1;  //操作完一个项目+1
 
		if (bItemNum >= 8)  //项目数达到8了,说明做完了一组压缩,将这一组数据写入文件,同时清空缓存
		{
			fwrite(bRestoreBuf, 1, bRestoreBufCnt, pfWrite);
			bItemNum = 0;
			memset(bRestoreBuf, 0, sizeof(bRestoreBuf));
			bRestoreBufCnt = 1;
		}
 
		//将刚刚匹配过的数据移出前向缓冲区
		for (i = 0; i < (wPreBufCnt - wMatchStringCnt); i++)
		{
			bPreBuf[i] = bPreBuf[i + wMatchStringCnt];
		}
		wPreBufCnt -= wMatchStringCnt;
 
		//如果滑动窗口将要溢出,先提前把前面的部分数据移出窗口
		if ((wWindowBufCnt + wMatchStringCnt) >  wWindowBufSize)
		{
			j = ((wWindowBufCnt + wMatchStringCnt) - wWindowBufSize);
			for (i = 0; i < (wWindowBufSize - j); i++)
			{
				bWindowBuf[i] = bWindowBuf[i + j];
			}
			wWindowBufCnt = wWindowBufSize - wMatchStringCnt;
		}
 
		//将刚刚匹配过的数据加入滑动窗口
		memcpy((BYTE *)&bWindowBuf[wWindowBufCnt], bMatchString, wMatchStringCnt);
		wWindowBufCnt += wMatchStringCnt;
	}
 
	//文件最后可能不满一组数据量,直接写到文件里
	if (0 != bRestoreBufCnt)
	{
		fwrite(bRestoreBuf, 1, bRestoreBufCnt, pfWrite);
	}
 
	fclose(pfRead);
	fclose(pfWrite);
 
	return 0;
}
 
DWORD LZSS_decode(char *pbReadFileName, char *pbWriteFileName)
{
	WORD i, j;
	BYTE bItemNum;
	BYTE bFlag;
	WORD wStart;
	WORD wMatchStringCnt = 0;
	WORD wWindowBufCnt = 0;
	FILE *pfRead = fopen(pbReadFileName, "rb");
	FILE *pfWrite = fopen(pbWriteFileName, "wb");
 
	while (0 != fread(&bFlag, 1, 1, pfRead))  //先读一个标记字节以确定接下来怎么解压数据
	{
		for (bItemNum = 0; bItemNum < 8; bItemNum++)  //8个项目为一组进行解压
		{
			//从标记字节的最高位开始解析,0代表原始数据,1代表(下标,匹配数)解析
			if (0 == (bFlag & ((BYTE)1 << (7 - bItemNum))))
			{
				if (fread(bPreBuf, 1, 1, pfRead) < 1)
				{
					goto LZSS_decode_out_;
				}
				fwrite(bPreBuf, 1, 1, pfWrite);
				bMatchString[0] = bPreBuf[0];
				wMatchStringCnt = 1;
			}
			else
			{
				if (fread(bPreBuf, 1, 2, pfRead) < 2)
				{
					goto LZSS_decode_out_;
				}
				//取出高位的滑动窗口匹配串下标
				wStart = ((WORD)bPreBuf[0] | ((WORD)bPreBuf[1] << 8)) / ((WORD)1 << bPreBufSizeBits);  
				//取出低位的匹配长度
				wMatchStringCnt = ((WORD)bPreBuf[0] | ((WORD)bPreBuf[1] << 8)) % ((WORD)1 << bPreBufSizeBits) + bThreshold;
				//将解压出的数据写入文件
				fwrite(&bWindowBuf[wStart], 1, wMatchStringCnt, pfWrite);
				memcpy(bMatchString, &bWindowBuf[wStart], wMatchStringCnt);
			}
 
			//如果滑动窗口将要溢出,先提前把前面的部分数据移出窗口
			if ((wWindowBufCnt + wMatchStringCnt) > wWindowBufSize)
			{
				j = (wWindowBufCnt + wMatchStringCnt) - wWindowBufSize;
				for (i = 0; i < wWindowBufCnt - j; i++)
				{
					bWindowBuf[i] = bWindowBuf[i + j];
				}
				wWindowBufCnt -= j;
			}
 
			//将解压处的数据同步写入到滑动窗口
			memcpy(&bWindowBuf[wWindowBufCnt], bMatchString, wMatchStringCnt);
			wWindowBufCnt += wMatchStringCnt;
		}
	}
 
LZSS_decode_out_:
 
	fclose(pfRead);
	fclose(pfWrite);
	return 0;
}

posted @ 2022-08-17 14:32  Mz1  阅读(979)  评论(0编辑  收藏  举报