长文件名在FAT文件系统上的实现
******************************
* 长文件名在FAT文件系统上的实现 *
******************************
2008/10/28 asdjf@163.com www.armecos.com
一般来说,嵌入式里使用DOS传统的“8+3”文件名格式基本上就够用了,但是在某些情况下,譬如为了和Windows/Linux操作系统无缝交互,最好支持长文件名。
为此,在《ecos增值包》里增加了FAT长文件名组件,实现如下功能:
1、支持英文版长文件名;
2、支持简体中文版长文件名;
3、同时支持Windows下UTF-16LE和Linux下UTF-8编码长文件名
以上所有功能都是可以配置裁减的,因为嵌入式系统里资源有限,你可以根据情况独立选择:是否只支持短文件名、是否支持英文长文件名、是否 支持中文长文件名、是否支持Windows下长文件名、是否支持Linux下长文件名等等,这样可以在资源和性能之间取得最佳平衡。
《ecos增值包》目前已经支持FAT12/16/32 over CF/SD/HDD(支持多逻辑盘分区),支持多FAT同步,支持 CACHE,支持HASH算法,支持长文件名,完全遵循UNIX I/O接口标准等等,完全满足嵌入式文件系统在性能和功能方面的要求。另外,还支持/准 备支持ROMFS、RAMFS、NAND Flash上的TrueFFS/JFFS2/YAFFS文件系统、USB Host、CDROM上的文件系统等 等。
================
英文版长文件名实现
================
先用实例说明:在CF卡上保存一个名为“longname12345678901234567890.txt”的英文长文件名文件,用WinHEX读取盘片目录扇区,得到如下数据:
0 1 2 3 4 5 6 7 8 9 A B C D E F
------------------------------------------------
43 39 00 30 00 2E 00 74 00 78 00 0F 00 F4 74 00 | C9.0...t.x...魌.
00 00 FF FF FF FF FF FF FF FF 00 00 FF FF FF FF | ..
02 36 00 37 00 38 00 39 00 30 00 0F 00 F4 31 00 | .6.7.8.9.0...?..
32 00 33 00 34 00 35 00 36 00 00 00 37 00 38 00 | 2.3.4.5.6...7.8.
01 6C 00 6F 00 6E 00 67 00 6E 00 0F 00 F4 61 00 | .l.o.n.g.n...鬭.
6D 00 65 00 31 00 32 00 33 00 00 00 34 00 35 00 | m.e.1.2.3...4.5.
4C 4F 4E 47 4E 41 7E 31 54 58 54 20 00 07 D6 99 | LONGNA~1TXT ..謾
58 39 58 39 00 00 D7 99 58 39 00 00 00 00 00 00 | X9X9..讬X9......
你会发现长文件名是倒着排放的(每目录项存13个字节文件名),最下面是短文件名LONGNA~1TXT
FAT32的一个重要的特点是完全支持长文件名。长文件名依然是记录在目录项中的。为了低版本的OS或程序能正确读取长文件名文件,系统 自动为所有长文件名文件创建了一个对应的短文件名,使对应数据既可以用长文件名寻址,也可以用短文件名寻址。不支持长文件名的OS或程序会忽略它认为不合 法的长文件名字段,而支持长文件名的OS或程序则会以长文件名为显式项来记录和编辑,并隐藏起短文件名。
当创建一个长文件名文件时,系统会自动加上对应的短文件名,其一般有的原则:
(1)、取长文件名的前6个字符加上"~1"形成短文件名,扩展名不变。
(2)、如果已存在这个文件名,则符号"~"后的数字递增,直到5。
(3)、如果文件名中"~"后面的数字达到5,则短文件名只使用长文件名的前两个字母。通过数学操纵长文件名的剩余字母生成短文件名的后四个字母,然后加后缀"~1"直到最后(如果有必要,或是其他数字以避免重复的文件名)。
(4)、如果存在老OS或程序无法读取的字符,换以"_"
长文件名的实现有赖于目录项偏移为0xB的属性字节,当此字节的属性为:只读、隐藏、系统、卷标,即其值为0FH时,DOS和WIN32 会认为其不合法而忽略其存在。这正是长文件名存在的依据。将目录项的0xB置为0F,其他就任由系统定义了,Windows9x或 Windows 2000、XP通常支持不超过255个字符的长文件名。系统将长文件名以13个字符为单位进行切割,每一组占据一个目录项。所以可能一个 文件需要多个目录项,这时长文件名的各个目录项按倒序排列在目录表中,以防与其他文件名混淆。
长文件名中的字符采用unicode形式编码,每个字符占据2字节的空间。
系统在存储长文件名时,总是先按倒序填充长文件名目录项,然后紧跟其对应的短文件名。从表2可以看出,长文件名中并不存储对应文件的文件 开始簇、文件大小、各种时间和日期属性。文件的这些属性还是存放在短文件名目录项中,一个长文件名总是和其相应的短文件名一一对应,短文件名没有了长文件 名还可以读,但长文件名如果没有对应的短文件名,不管什么系统都将忽略其存在。所以短文件名是至关重要的。在不支持长文件名的环境中对短文件名中的文件名 和扩展名字段作更改(包括删除,因为删除是对首字符改写E5H),都会使长文件名形同虚设。长文件名和短文件名之间的联系光靠他们之间的位置关系维系显然 远远不够。其实,长文件名的0xD字节的校验和起很重要的作用,此校验和是用短文件名的11个字符通过一种运算方式来得到的。系统根据相应的算法来确定相 应的长文件名和短文件名是否匹配。这个算法不太容易用公式说明,我们用一段c程序来加以说明。
假设文件名11个字符组成字符串shortname[],校验和用chknum表示。得到过程如下:
int i,j,chknum=0;
for (i=11; i>0; i--)
chksum = ((chksum & 1) ? 0x80 : 0) + (chksum >> 1) + shortname[j++];
如果通过短文件名计算出来的校验和与长文件名中的0xD偏移处数据不相等。系统无论如何都不会将它们配对的。
-----------------------------------------------------
| 表1 FAT32短文件目录项32个字节的表示定义 |
-----------------------------------------------------
|字节偏移(16进制) | 字节数 | 定义 |
-----------------------------------------------------
| 0x0~0x7 | 8 | 文件名 |
-----------------------------------------------------
| 0x8~0xA | 3 | 扩展名 |
-----------------------------------------------------
| | | | 00000000(读写) |
| | | | 00000001(只读) |
| | | 属| 00000010(隐藏) |
| 0xB* | 1 | 性| 00000100(系统) |
| | | 字| 00001000(卷标) |
| | | 节| 00010000(子目录) |
| | | | 00100000(归档) |
-----------------------------------------------------
| 0xC | 1 | 系统保留 |
-----------------------------------------------------
| 0xD | 1 | 创建时间的10毫秒位 |
-----------------------------------------------------
| 0xE~0xF | 2 | 文件创建时间 |
-----------------------------------------------------
| 0x10~0x11 | 2 | 文件创建日期 |
| 0x12~0x13 | 2 | 文件最后访问日期 |
| 0x14~0x15 | 2 | 文件起始簇号的高16位 |
| 0x16~0x17 | 2 | 文件的最近修改时间 |
| 0x18~0x19 | 2 | 文件的最近修改日期 |
| 0x1A~0x1B | 2 | 文件起始簇号的低16位 |
| 0x1C~0x1F | 4 | 表示文件的长度 |
-----------------------------------------------------
*此字段在短文件目录项中不可取值0FH,如果设值为0FH,目录段为长文件名目录段
------------------------------------------------------
| 表2 FAT32长文件目录项32个字节的表示定义 |
------------------------------------------------------
| 字节偏移 | 字节数 | 定义 |
| (16进制) | | |
------------------------------------------------------
| | | | 7 保留未用 |
| | | 属| 6 1表示长文件最后一个目录项 |
| | | 性| 5 保留未用 |
| | | 字| 4 |
| 0x0 | 1 | 节| 3 |
| | | 位| 2 顺序号数值 |
| | | 意| 1 |
| | | 义| 0 |
------------------------------------------------------
| 0x1~0xA | 10 | 长文件名unicode码 |
| 0xB | 1 | 长文件名目录项标志,取值0FH |
| 0xC | 1 | 系统保留 |
| 0xD | 1 | 校验值(根据短文件名计算得出) |
| 0xE~0x19 | 12 | 长文件名unicode码 |
| 0x1A~0x1B | 2 | 文件起始簇号(目前常置0) |
| 0x1C~0x1F | 4 | 长文件名unicode码 |
------------------------------------------------------
只要按照上述格式保存长文件名目录项,就可以实现长文件名了,需要修改的部分是打开、删除、改名等文件系统函数。先生成若干长文件名目录 项,倒序排列,计算校验和,填写序号,设置0F标志,UNICODE转换,设置最后目录项,填充FF结束。然后,按要求生成短文件名目录项。
====================
简体中文版长文件名实现
====================
上面的方法可以实现英文长文件名,但如果想支持简体中文,还需要支持GB和UNICODE互换,ASCII和GB混排。
---------------
GB和UNICODE互换
---------------
UNICODE转GB和GB转UNICODE没什么好办法,一般通过查表法实现。
unsigned short int unigb_table[7446][2]={
0x00a4,0xa1e8,
0x00a7,0xa1ec,
0x00a8,0xa1a7,
0x00b0,0xa1e3,
0x00b1,0xa1c0,
......
0xffe0,0xa1e9,
0xffe1,0xa1ea,
0xffe3,0xa3fe,
0xffe5,0xa3a4,
0xffff,0x0000
};
上面的表格是按UNICODE增序排列的UNICODE编码到GB编码转换对照表。因为该表为顺序表,所以可以采用二分查表法大大加快查询速度。没办法,这个表就是有点大,如果不需要支持简体中文,最好把它配置裁减掉,以便节省存储空间。
将上表反过来就可以实现GB编码到UNICODE编码转换对照表gbuni_table[7446][2]。一个小技巧:直接用 EXCEL打开unigb_table数据部分,选择逗号做分割符,交换两列位置,按GB码增序排序,然后再插入两列,输入逗号,拖下去,产生两列逗号, 保存之,再稍加修改即可生成增序排列的GB编码到UNICODE编码转换对照表。
-------------
ASCII和GB混排
-------------
ASCII字符用一个字节表示,GB汉字用两个字节表示,如果ASCII和GB汉字混排,那么如何把两者区分开呢?
先从ASCII说起。ASCII是用来表示英文字符的一种编码规范,每个ASCII字符占用1个字节(8bits),因此,ASCII编 码可以表示的最大字符数是256,其实英文字符并没有那么多,一般只用前128个(最高位为0),其中包括了控制字符、数字、大小写字母和其他一些符号。 而最高位为1的另128个字符被称为“扩展ASCII”,一般用来存放英文的制表符、部分音标字符等等的一些其他符号,这种字符编码规范显然用来处理英文 没有什么问题。(实际上也可以用来处理法文、德文等一些其他的西欧字符,但是不能和英文通用),但是面对中文、阿拉伯文之类复杂的文字,255个字符显然 不够用。于是,各个国家纷纷制定了自己的文字编码规范,其中中文的文字编码规范叫做“GB2312-80”,它是和ASCII兼容的一种编码规范,其实就 是利用扩展ASCII没有真正标准化这一点,把一个中文字符用两个扩展ASCII字符来表示。但是这个方法有问题,最大的问题就是,中文文字没有真正属于 自己的编码,因为扩展ASCII码虽然没有真正的标准化,但是PC里的ASCII码还是有一个事实标准的(存放着英文制表符),所以很多软件利用这些符号 来画表格。这样的软件用到中文系统中,这些表格符就会被误认作中文字,破坏版面。而且,统计中英 文混合字符串中的字数,也是比较复杂的,我们必须判断一 个ASCII码是否扩展,以及它的下一个ASCII是否扩展,然后才“猜”那可能是一个中文字。
在《ecos增值包》里,不需要这么复杂,我们把00-7F之间的字符标识为ASCII字符,大于7F的字符认为是汉字,一次操作两字节。这样就能将英文和汉字区别出来分别编码了。
--------------
各种字符编码标准
--------------
为了更清楚地了解《ecos增值包》采用的编码标准,建议详细了解以下术语的确切含义:
ASCII
iso8859-1
GB码
GB2312
GB12345-90
GBK
GB18030
BIG5编码
UTF-8
UTF-16(大端、小端)
UCS2、UCS4
==============================
Windows和Linux长文件名编码的不同
==============================
上面说明GB和UNICODE互换时为了简化描述,我们只是笼统地提到UNICODE编码,其实,Windows和Linux采用的UNICODE编码格式是不同的。Windows采用UTF-16LE格式,Linux采用UTF-8格式。
为什么不直接使用UNICODE而非要采用两种不同的UTF格式呢?这里面是有历史渊源的。
-------
Windows
-------
首先介绍Windows。
早期的Windows系统,例如Windows 1.0、Windows 2.0、Windows 3.0、 Windows 3.1等 系统,是不支持Unicode编码的,而是使用ANSI编码(即兼容ASCII的任何本地编码)。即使在Windows 9x系列系统中,也不支持 Unicode编码,只是在文件系统中使用UTF-16编码(例如FAT16/FAT32的长文件名),系统核心还是使用ANSI编码。
微软真正开始实现Unicode编码的是Windows NT操作系统。后来的Windows 2000/XP/Vista都是基于Windows NT内核的。
微软创造Windows NT系统的最初目的是抢占服务器和工作站市场,微软公司从数字设备公司(Digital Equipment Corporation)雇佣了一批人员来开发这个新系统,其竞争对手为Unix系列操作系统。
由于Windows NT是全新设计的系统,没有历史累赘,一开始就决定使用Unicode编码作为默认编码。于是,NT系统内核强制一 定要使用Unicode编码。那时,Unicode只有一种形式,就是UTF-16,所以Windows的Unicode实现方案是使用UTF-16。这 样就决定NT的内核只能使用一种字符串,就是UTF-16字符串,用C语言来说,就是wchar_t类型的字符串,每个字符占用2个字节。看过《快快乐乐 跟我学WDM驱动》的读者一定知道在WDM驱动里也不使用普通的字符,而是全都采用UNICODE,原因就在这里,因为驱动相当于内核补丁,而NT内核强 制使用UNICODE编码,所以,WDM驱动里也必须使用UNICODE。呵呵,初次接触WDM驱动的人肯定对字符串操作感觉怪怪的,习惯就好了,而且, 越用越能体会到使用UNICODE的优势。这不,连USB的字符串描述符也强制要求使用UNICODE。Windows里采用的UTF-16更准确地说是 小端UTF-16。UTF-16可看成是UCS-2的父集。在没有辅助平面字符前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符 后,就只称为UTF-16了。现在若有软件声称自己支援UCS-2编码,那其实是暗指它不能支援辅助平面字符的委婉语。
文件系统方面,微软扩展了FAT16/32,增加了长文件名功能;还有就是重新设计了NTFS文件系统。FAT16/FAT32在DOS 下,只能使用8+3的文件名长度,使用的是ANSI编码。在Windows下,通过使用多个目录项来实现长文件名,并强制性地把长文件名的编码设定为 Unicode的UTF-16编码,这样有利于国际化。NTFS文件系统专门为NT系统设计,自然也是使用UTF-16编码。
在Windows下编程,最好直接使用Unicode编程,这样可以避免使用ANSI字符串而在不同语系的Windows系统里产生乱码现象,更因为少了字符串转换这步,可以加快程序运行速度。
-------------
Unix类操作系统
-------------
Unix产生于1969年,那时还没有出现Unicode呢,ASCII编码是那时最完善的编码。那时硬件十分昂贵,就连使用两个字节来表示一个字符也是不敢想象的。所以Unix系统是使用单字节编码来表示文字字符,其内核并没有强制使用哪种字符编码。
也就是说,Unix类的系统中,在内核里,是不会管你字符编码是什么的,也不会管你会不会发生乱码,只要是以0结尾的字节串,都是有效的字符串。
由于最初Unix的这种设计,以及大量使用了流技术,这导致Unix无法使用UTF-16字符编码,因为UTF-16是双字节的。
Unicode组织后来设计了UTF-8方案,是一种8位的Unicode编码方法。UTF-8的出现使得Unix类系统支持Unicode成为可能。这样Unix类的系统就不需要进行重新设计,而通过UTF-8来实现Unicode支持。
使用UTF-8的原因:
ASCII转换成UCS-2,在编码前插入一个0x0。用这些编码,会含括一些控制符,比如 " 或 '/',这在UNIX和一些C函数中,将会产生严重错误。因此可以肯定,UCS-2不适合作为Unicode的外部编码,也因此诞生了UTF-8。
设计UTF-8的理由:
UTF-8的设计有以下的多字符组序列的特质:
单字节字符的最高有效位元永远为0;
多字节序列中的首个字符组的几个最高有效位元决定了序列的长度。最高有效位为110的是2字节序列,而1110的是三字节序列,如此类推;
多字节序列中其余的字节中的首两个最高有效位元为10。
Linux是一个Unix的复制品,Unix的这种设计也复制到了Linux。
文件系统方面,Unix类系统也没有强制用户使用UTF-8编码,只要是ASCII兼容的字符编码都可以用,这样有好处也有坏处,好处是系统内核不必理会编码问题,坏处是会造成编码混乱。
在UTF-8发布之前,Linux 用户使用各种不同特定语言的扩展 ASCII,像欧洲用户 用 ISO 8859-1 或 ISO 8859-2,希腊用户使用 ISO 8859-7,俄罗斯用户使用 KOI-8 / ISO 8859-5 /CP1251(西里尔字母)。这使得数据交换出现了很多问题,并且需要为这些编码之间的差异编写应用软件。这种语言支持是不完善的,而且数据交换没有经 过测试。Linux 主要的发行商和应用程序开发者正致力于让主要以 UTF-8 格式表示的 Unicode 成为 Linux 中的标准。
Unix类系统的Unicode实现与Windows不同,两者比较如下:
(1)Windows采取的方案是UTF-16,系统在内核里面强制使用UTF-16编码,甚至连文件系统也必须是UTF-16字符编码 格式,应用层可以不需要理会codepage设置而直接使用UTF-16,也可以把ANSI字符串转换为UTF-16字符串再调用内核。
(2)Unix类的系统采取的方案是UTF-8,内核不理会字符编码的具体实现,Unicode的实现是在应用层上实现,主要是通过libc的locale功能进行实现。必须把locale设置为UTF-8才可以使用。
----------------------------------
《ecos增值包》对不同编码长文件名的支持
----------------------------------
考虑到Windows和Linux都比较流行,所以决定两者都支持。提供静态配置选项,根据实际情况选择,还支持动态加载字符编码集,动态支持不同系统的盘片。
下面是两个字符集函数表,通过register_nls(nls_table);向系统注册字符集操作函数表,使用时调用load_nls("charset");加载字符集相应操作函数表就可以正确编解码了。
static struct nls_table default_table = {
.charset = "default", //字符集名称
.uni2char = uni2char, //UNICODE解码
.char2uni = char2uni, //UNICODE编码
.charset2lower = charset2lower,
.charset2upper = charset2upper,
};
static struct nls_table table = {
.charset = "utf8",
.uni2char = uni2char,
.char2uni = char2uni,
.charset2lower = identity, /* no conversion */
.charset2upper = identity,
};
==============
测试长文件名组件
==============
1、使用《ecos增值包》,通过EASYARM2200/SMARTARM2200开发板在CF/SD卡上创建长文件名文件,在Windows中打开,如显示正确,则创建成功。
中英文混排:
fd = open( "长文件名文件演示longname1234567890.txt", O_WRONLY|O_CREAT );
或
纯英文:
fd = open( "longname12345678901234567890.txt", O_WRONLY|O_CREAT );
2、使用Windows在CF/SD卡上创建若干长文件名,通过EASYARM2200/SMARTARM2200开发板列出卡上的目录和文件,如显示正确,则长文件名解码成功。
列出根目录下目录名和文件名:
listdir( "/", true, -1, &existingdirents );
转自:http://www.ourdev.cn/bbs/bbs_content.jsp?bbs_sn=3563451&bbs_page_no=2&bbs_id=3004