其实写完前面的关于FAT文件系统的簇检查那一部分之后,我一直没准备写第二部分关于文件目录项处理这一部分,因为这部分都是按照FAT规范来处理的。
这段是处理fat文件系统的目录项的相关代码,其中root->dir是构造出来的。因为FAT的根目录在磁盘介质中没有实际的元数据。readDosDirSection读出一个目录项下的所有子目录项(这里的目录项是一个统称,包括文件file和目录direntory。
Name |
Offset(byte) |
Size(byte) |
description |
DIR_NAME |
0 |
11 |
这11个字节又分为两段,其中前8个字节为文件名,后三个字节为文件的扩展属性,比如sample.txt,文件名为sample,扩展文件名为txt |
DIR_Attr |
11 |
1 |
文件属性: ATTR_READ_ONLY 0x1 ATTR_HIDDEN 0X2 ATTR_SYSTEM 0X4 ATTR_VOLUME_ID 0X8 ATTR_DIRECTORY 0X10 ATTR_ARCHIVE 0X20 ATTR_LONG_NAME =ATTR_READ_ONLY|ATTR_HIDDEN|ATTR_SYSTEM |ATTR_VOLUME_ID |
DIR_NTRes |
12 |
1 |
Windows NT用,现在设置为0 |
DIR_CrtTimeTenth |
13 |
1 |
文件创建时间,微秒段 |
DIR_CrtTime |
14 |
2 |
文件创建时间 |
DIR_CrtDate |
16 |
2 |
文件创建日期 |
DIR_LstAccDate |
18 |
2 |
文件的最后访问时间 |
DIR_FstClusHI |
20 |
2 |
该文件首簇的高16bit(FAT12和FAT16中设置为0) |
DIR_WrtTime |
22 |
2 |
文件的最后写的时间 |
DIR_WrtDate |
24 |
2 |
文件最后写的日期 |
DIR_FstClustLO |
26 |
2 |
文件首簇的低16bit |
DIR_FileSize |
28 |
4 |
文件的大小 |
他们是根据DIR_Attr字段来区分的,需要说明的是,在FAT32中,bootsector中指明了rootdir所在的位置,一般是cluster = 2的位置(即数据区的第一个簇)。但是在FAT12和FAT16中,该字段是0。
readDosDirSection读出一个文件夹下的所有目录项,将其添加到pendingDirectories链表中,这个链表中的所有目录项都是等待被处理的。
readDosDirSection这个函数特别长,我们以阅读代码的方式来一段一段的分析。
我们知道目录项中的DIR_FstClusHI指定了该文件簇链的第一个簇的位置。必须是在数据区的,即在CLUST_FIRST和boot->NumClusters之间。上面这段代码跳过了这种情况:
(1) 该目录项是一个文件夹,但是该文件夹是空的,即没有子文件,那么其dir->head必然等于0,对于空文件,没有必要进一步检查其子目录项。
do {
if (!(boot->flags & FAT32) && !dir->parent) {
//in FAT12 or FAT16,each direntry take over 32 bytes
last = boot->RootDirEnts * 32;
off = boot->ResSectors + boot->FATs * boot->FATsecs;
} else {//FAT32
last = boot->SecPerClust * boot->BytesPerSec;
//caculate the offset ,because the first data sector is NO.2
off = cl * boot->SecPerClust + boot->ClusterOffset;
}
这段代码使用来处理FAT12 和FAT16的根目录的,因为他们的boot->rootdir字段等于0.所以根据bootsector是FAT表的大小来算rootdir的偏移。
Bootsector(ResSectors) |
FAT表 |
根目录区 |
数据区 |
off *= boot->BytesPerSec;
if (lseek64(f, off, SEEK_SET) != off) {
printf("off = %llu\n", off);
perror("Unable to lseek64");
return FSFATAL;
}
if (read(f, buffer, last) != last) {
perror("Unable to read");
return FSFATAL;
}
last /= 32;
上面这段通过rootdir的首簇的位置,来读出这段数据,下一步就是开始解析了。其中last是在该sector中最后一个目录项的下标,因为不管是长目录项还是短目录项,其大小都是32比特。
for (p = buffer, i = 0; i < last; i++, p += 32) {
if (dir->fsckflags & DIREMPWARN) {
*p = SLOT_EMPTY;
continue;
}
如果fsckflags标志位DIREMPWARN被设置的话,那么就将剩下的所有目录项设置为SLOT_EMPTY,至于为什么,下面再仔细讲解。
if (*p == SLOT_EMPTY || *p == SLOT_DELETED) {
if (*p == SLOT_EMPTY) {
dir->fsckflags |= DIREMPTY;
empty = p;
empcl = cl;
}
continue;
}
上面这段文字是MS的FAT spec中关于SLOT_EMPTY的解释。是说如果发现一个目录项的第一个字节是0x00,那么就意味着在它之后的所有目录项同样是空的。如果一切正常的,那么就会一直执行上面的代码,因为它之后所有目录项的第一个字节同样是0x00。但是凡事都有意外,fsck的目的就是修复这些意外的情况。
if (dir->fsckflags & DIREMPTY) {
if (!(dir->fsckflags & DIREMPWARN)) {
如果执行到此处,表示出现了意外,因为在SLOT_EMPTY之后出现了一些正常的目录项。从下面的打印信息也可以看出来,has entries after end of directory。
pwarn("%s has entries after end of directory\n",fullpath(dir));
if (ask(1, "Extend")) {
u_char *q;
dir->fsckflags &= ~DIREMPTY;
if (delete(f, boot, fat,empcl, empty - buffer,cl, p - buffer, 1) == FSFATAL)
delete用于删除属于该dir中一些目录项,需要注意的,这些dir可能是跨簇的,而且这些簇可能并不连续。需要从FAT中查询获得。准确的说,是正确的利用了第一阶段簇检查的结果。
return FSFATAL;
q = empcl == cl ? empty : buffer;
for (; q < p; q += 32)
*q = SLOT_DELETED;
mod |= THISMOD|FSDIRMOD;
} else if (ask(1, "Truncate"))
dir->fsckflags |= DIREMPWARN;
截断操作,注意这儿设置了DIREMPWARN标志位,这样就会出现了最开始出现的关于DIREMPWARN标志位的判断。如果DIREMPWARN,那么就将剩下的所有目录项的第一个字节设置为SLOT_EMPTY。
}
if (dir->fsckflags & DIREMPWARN) {
*p = SLOT_DELETED;
mod |= THISMOD|FSDIRMOD;
continue;
} else if (dir->fsckflags & DIREMPTY)
mod |= FSERROR;
empty = NULL;
}
这样关于目录项的检查就算通过了,下面就是开始解析正常的目录项了。
if (p[11] == ATTR_WIN95) {
// ATTR_WIN95是LDIR_Attr中的一个标志位,用来标识该目录项是长目录。
Name |
Offset(byte) |
Size (bytes) |
description |
LDIR_Ord |
0 |
1 |
长目录项的index 如果0x40,那么就是长目录项的最后一个 |
LDIR_Name1 |
1 |
10 |
长名字的第1-5个字符 |
LDIR_Attr |
11 |
1 |
属性 |
LDIR_Type |
12 |
1 |
如果是0,表示该长目录项是长文件名的一部分 |
LDIR_Chksum |
13 |
1 |
校验和,用于检验长文件名的完整性 |
LDIR_Name2 |
14 |
12 |
长文件名的第6-11个字符 |
LDIR-FstClustLO |
26 |
2 |
必须为0 |
LDIR_Name3 |
28 |
4 |
长文件名的第12-13个字符 |
if (*p & LRFIRST) {
//从上面的表格可以看出,长目录项的第一个字节用来标示该长目录项的下标,但是凡事都有第一。那么LRFIRST (0x40)用来表示第一个长目录项。我们知道要正确的解析一个长文件名的话,需要读出全部的长目录项来解析,要不然算出的校验和就不对。
if (shortSum != -1) {
if (!invlfn) {
invlfn = vallfn;
invcl = valcl;
}
}
memset(longName, 0, sizeof longName);
shortSum = p[13];
vallfn = p;
valcl = cl;
其中vallfn和valcl是用来记录一个长目录项的起始位置的,因为一旦出错,后面就有可能删除相关的全部的目录项
} else if (shortSum != p[13]
|| lidx != (*p & LRNOMASK)) {
if (!invlfn) {
invlfn = vallfn;
invcl = valcl;
}
if (!invlfn) {
invlfn = p;
// 注意上面的vallfn 与这儿的invlfn的差别,invlfn前面的in是invalid的意思,是在出错情况下用来保存目录项的位置的,以便将来delete或者truncate。
invcl = cl;
}
vallfn = NULL;
}
下面的代码我就不贴了,就是具体的分析计算出每一个长目录项所包含的名字部分。需要注意的是长目录项中的LDIR-FstClustLO字段必须为0,因为这个字段是用来指示文件或者文件夹所对应的第一个簇。但是长目录项是一个附加项,是在短目录项不能完整的表达文件名的时候,用来存储文件名的,除此之外,没有其余的用途。
/*
* This is a standard msdosfs directory entry.
*/
长目录项之后就是一个标准的短目录项,不管是文件还是目录,都有相应的短目录项,但是并不一定有相应的长目录项。
memset(&dirent, 0, sizeof dirent);
/*
* it's a short name record, but we need to know
* more, so get the flags first.
*/
dirent.flags = p[11];
/*
* Translate from 850 to ISO here XXX
*/
for (j = 0; j < 8; j++)
dirent.name[j] = p[j];
dirent.name[8] = '\0';
for (k = 7; k >= 0 && dirent.name[k] == ' '; k--)
dirent.name[k] = '\0';
if (dirent.name[k] != '\0')
k++;
if (dirent.name[0] == SLOT_E5)
dirent.name[0] = 0xe5;
SLOT_E5是一个特别的字符,为0xE5,前面的SLOT_DELETED正好是0Xe5,所以在需要真正用到0XE5的时候,就要0x5来代替,所以这儿需要进行转义。有人会问那0X5本身呢。如果你查码表的话,会发现0X5是个特殊的字符,在FAT中不用。
if (dirent.flags & ATTR_VOLUME) {
if (vallfn || invlfn) {
mod |= removede(f, boot, fat,
invlfn ? invlfn : vallfn, p,
invlfn ? invcl : valcl, -1, 0,
fullpath(dir), 2);
vallfn = NULL;
invlfn = NULL;
}
continue;
}
ATTR_VOLUME是一个特殊的标志位,看看spec的解释吧。
同时需要注意的是,长目录项中也设置了该标志位,但是短目录项中并没有。
if (vallfn && shortSum != calcShortSum(p)) {
if (!invlfn) {
invlfn = vallfn;
invcl = valcl;
}
vallfn = NULL;
}
比较短目录项中计算出来的校验和与长目录项中计算的校验和是否相等。
dirent.parent = dir;
dirent.next = dir->child;
设置其父指针,这样就会在内存中构造一个文件目录树。
下面的比较简单,不一一列出,但是需要注意几点:
(1) 对于文件夹而言,其dirent.size == 0为0
(2) 如果一个目录项代表的是一个文件,那么需要校验文件的大小。如果其簇链长度超出了文件的长度,那么就需要截断。被截断的内容将来会被放置到LOST.DIR中,因为它是无主的,即没有对应的目录项。
(3) 当一个空文件夹被创建时,会在其下建立两个特殊的目录项,就是“.”和“..”。其中dot的dir->head等于其本身dir的head,而dotdot的head等于dir->parent->head.
(4) 对于一个dir,检测合格之后会通过下面的代码
n->next = pendingDirectories;
n->dir = d;
pendingDirectories = n;
添加到链表中,以便后面检测它的子目录项。但是对于”.”和”..”,跳过了这一步。因为如果将其也添加到pendingDirectories队列中的话,就会陷入死循环中。为什么?留给读者自己去理解。