引入
要深入了解sqlite数据库,数据库文件的存储格式必须熟悉。
在分析sqlite数据库文件时,选择用php做分析工具。在做文本分析方面,php差不多是最方便的工具了,当然用c语言也可以,不过c语言擅长的是高效的执行,在处理文本串时,就显得太烦琐了。
php真是一个好东西,好多人只把它当作一个编写网页的工具。其实它在命令行工具里面也很出色,比起perl,tcl来说,优点更突出。这次选php来分析sqlite数据库文件的格式,就是看中了它的简单易用,功能强大。准备用这把瑞士军刀好好地剖析一下sqlite的数据库文件格式。
sqlite官网上文件格式的描述文档,很详细,英文好的朋友可以细看。
网上“空转”先生的一篇文章《SQLite数据库文件格式全面分析》,写得不错,是用中文写的。需要的朋友可以在百度文库中搜一下。
”空转“的文章给我很大的启发,他用了实际的例子做引导,很对我的胃口。他的文章主要是用16进制文件编辑器来对文件格式做说明,我则使用php对sqlite的数据库文件做更细致地分析。
本系列探索sqlite数据库文件格式,本篇文章用于分析数据库文件的前100个字节的文件头。sqlite官网上有详细说明,我引用sqlite源文件btreeInt.h中的相关说明部分如下:
** The first page is always a btree page. The first 100 bytes of the first
** page contain a special header (the "file header") that describes the file.
** The format of the file header is as follows:
**
** OFFSET SIZE DESCRIPTION
** 0 16 Header string: "SQLite format 3\000"
** 16 2 Page size in bytes. (1 means 65536)
** 18 1 File format write version
** 19 1 File format read version
** 20 1 Bytes of unused space at the end of each page
** 21 1 Max embedded payload fraction (must be 64)
** 22 1 Min embedded payload fraction (must be 32)
** 23 1 Min leaf payload fraction (must be 32)
** 24 4 File change counter
** 28 4 Reserved for future use
** 32 4 First freelist page
** 36 4 Number of freelist pages in the file
** 40 60 15 4-byte meta values passed to higher layers
**
** 40 4 Schema cookie
** 44 4 File format of schema layer
** 48 4 Size of page cache
** 52 4 Largest root-page (auto/incr_vacuum)
** 56 4 1=UTF-8 2=UTF16le 3=UTF16be
** 60 4 User version
** 64 4 Incremental vacuum mode
** 68 4 Application-ID
** 72 20 unused
** 92 4 The version-valid-for number
** 96 4 SQLITE_VERSION_NUMBER
**
** All of the integer values are big-endian (most significant byte first).
这里面最后一句话值得注意,所有的整数都是大端形式,因为大端形式更符合人的从左自右的自然阅读习惯
为了方便用php处理文件头,本人对照sqlite源文件btreeInt.h中的文件头描述手工编写了json格式的文件头描述对象
文件头的描述
下面是文件头的描述文件,json格式
{
"fileHeader":
{"size":"unused",
"elements":
{
"HeaderString":{"offset":0,"size":16,"type":"char"},
"PageSize": {"offset":16,"size":2,"type":"u16"},
"writeVersion": {"offset":18,"size":1,"type":"u8"},
"readVersion": {"offset":19,"size":1,"type":"u8"},
"unusedSpace": {"offset":20,"size":1,"type":"u8"},
"MaxEmbeddedFraction": {"offset":21,"size":1,"type":"u8"},
"MinEmbeddedFraction": {"offset":22,"size":1,"type":"u8"},
"MinLeafFraction": {"offset":23,"size":1,"type":"u8"},
"FileChangeCounter": {"offset":24,"size":4,"type":"u32"},
"Reserved": {"offset":28,"size":4,"type":"u32"},
"FirstFreelistPage": {"offset":32,"size":4,"type":"u32"},
"NumberOfFreelistPages": {"offset":36,"size":4,"type":"u32"},
"SchemaCookie": {"offset":40,"size":4,"type":"u32"},
"FileFormat": {"offset":44,"size":4,"type":"u32"},
"SizeOfPageCache": {"offset":48,"size":4,"type":"u32"},
"LargestRootPage": {"offset":52,"size":4,"type":"u32"},
"UTF-Type": {"offset":56,"size":4,"type":"u32"},
"UserVersion": {"offset":60,"size":4,"type":"u32"},
"IncVacuumMode": {"offset":64,"size":4,"type":"u32"},
"Application-ID": {"offset":68,"size":4,"type":"u32"},
"unused": {"offset":72,"size":20,"type":"char"},
"version-valid": {"offset":92,"size":4,"type":"u32"},
"VERSION_NUMBER": {"offset":96,"size":4,"type":"u32"}
}
}
}
文件头解析成文本
下面是文件头的解析代码:
<?php
//将sqlite数据库文件的最前面的100个字节的文件头解析成文本形式
$outPath = "workingLog";
$i = 1;
$headerFileName = $outPath.'/page'.$i.'-header.dat';
$headerSize =filesize($headerFileName);
echo $headerFileName,'->size = ',$headerSize," bytes\n";
$fin = fopen($headerFileName,'rb');
//根据文件头描述json,将文件内容解析成文本
$jsonArray = json_decode(file_get_contents('structJson/fileHeader.json'),true);
$elements = $jsonArray["fileHeader"]["elements"];
//print_r($headerElements );
echo "start unpacking...\n\n";
$result='';
foreach ($elements as $name => $info) {
//fseek($fin, $info['offset']*1);
$rawVal = fread($fin, $info['size']*1);
$val = unpackValue($rawVal,$info['size']*1,$info['type']);
$hexOffset = int2HexStr($info['offset']);
$result .= '0x'.$hexOffset.' -> '.$name .' : '.$val."\n";
}
fclose($fin);
echo $result;
echo "\nunpack over!\n";
//将原始值解包成可读值
function unpackValue($rawVal,$size,$type){
if($type=='char'){
$a = unpack('A*s', $rawVal);
return $a['s'];
}
if($type=='u8'){
$a = unpack('Cs', $rawVal);
return $a['s'];
}
if($type=='u16'){
$a = unpack('ns', $rawVal);
return $a['s'];
}
if($type=='u32'){
$a = unpack('Ns', $rawVal);
return $a['s'];
}
return 'unpackError';
}
//将整数转成16进制串
//将整形转换成16进制显示时,应先pack成真正的整型字节存储形式,然后再以16进制格式unpack
function int2HexStr($int){
//echo $int,"\n";
//转换成16位整数字节数组时,选择大端序,在字节顺序上符合人们的一般习惯,即高位在前(在字节流的前面)。
$raw = pack('n',$int);
//转换成16进制串时,一次性吃进半个字节,所以这里要解包4次,
//一般人的阅读习惯,16进制表示的字符串都是高位(半字节)在前,所以格式符选H而不选h
return unpack('H4s',$raw)['s'];
}
下图是用php对文件头解析后的结果:可以清楚地查看文件头各个字段的含义,第一列是各个字段的偏移量,第二列是各个字段的名字,紧接着的就是各个字段的具体值。各个字段的具体含义请参照官网文献或“空转”先生的文章
下图是用16进制编辑软件显示的文件头的16进制数据形式,大家可以对比一下,用文本方式查看要比用16进制方式查看清楚多了。