android fsck_msdos 分析(二):目錄項
其實寫完前面的關於FAT檔案系統的簇檢查那一部分之後,我一直沒準備寫第二部分關於檔案目錄項處理這一部分,因為這部分都是按照FAT規範來處理的。
- handleDirTree(int dosfs, struct bootblock *boot, struct
fatEntry *fat)
- {
- int mod;
- mod = readDosDirSection(dosfs, boot, fat, rootDir);
- if (mod & FSFATAL)
- return FSFATAL;
- /*
- *
- */
- while (pendingDirectories) {
- struct dosDirEntry *dir = pendingDirectories->dir;
- struct dirTodoNode *n = pendingDirectories->next;
- /*
- * remove TODO entry now, the list might change during
- * directory reads
- */
- freeDirTodo(pendingDirectories);
- pendingDirectories = n;
- /*
- * handle subdirectory
- */
- mod |= readDosDirSection(dosfs, boot, fat, dir);
- if (mod & FSFATAL)
- return FSFATAL;
- }
- return mod;
- }
這段是處理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這個函式特別長,我們以閱讀程式碼的方式來一段一段的分析。
- cl = dir->head;
- if (dir->parent && (cl < CLUST_FIRST || cl >= boot->NumClusters)) {
- /*
- * Already handled somewhere else.
- */
- return FSOK;
- }
我們知道目錄項中的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佇列中的話,就會陷入死迴圈中。為什麼?留給讀者自己去理解。