长文件名在FAT文件系统上的实现

长文件名在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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值