diff options
Diffstat (limited to '')
-rw-r--r-- | dosfstools/src/lfn.c | 502 |
1 files changed, 502 insertions, 0 deletions
diff --git a/dosfstools/src/lfn.c b/dosfstools/src/lfn.c new file mode 100644 index 000000000..736491cb0 --- /dev/null +++ b/dosfstools/src/lfn.c @@ -0,0 +1,502 @@ +/* lfn.c - Functions for handling VFAT long filenames + + Copyright (C) 1998 Roman Hodek <Roman.Hodek@informatik.uni-erlangen.de> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + On Debian systems, the complete text of the GNU General Public License + can be found in /usr/share/common-licenses/GPL-3 file. +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include <time.h> + +#include "common.h" +#include "io.h" +#include "dosfsck.h" +#include "lfn.h" +#include "file.h" + +typedef struct { + __u8 id; /* sequence number for slot */ + __u8 name0_4[10]; /* first 5 characters in name */ + __u8 attr; /* attribute byte */ + __u8 reserved; /* always 0 */ + __u8 alias_checksum; /* checksum for 8.3 alias */ + __u8 name5_10[12]; /* 6 more characters in name */ + __u16 start; /* starting cluster number, 0 in long slots */ + __u8 name11_12[4]; /* last 2 characters in name */ +} LFN_ENT; + +#define LFN_ID_START 0x40 +#define LFN_ID_SLOTMASK 0x1f + +#define CHARS_PER_LFN 13 + +/* These modul-global vars represent the state of the LFN parser */ +unsigned char *lfn_unicode = NULL; +unsigned char lfn_checksum; +int lfn_slot = -1; +loff_t *lfn_offsets = NULL; +int lfn_parts = 0; + +static unsigned char fat_uni2esc[64] = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '+', '-' +}; + +/* This defines which unicode chars are directly convertable to ISO-8859-1 */ +#define UNICODE_CONVERTABLE(cl,ch) (ch == 0 && (cl < 0x80 || cl >= 0xa0)) + +/* for maxlen param */ +#define UNTIL_0 INT_MAX + +/* Convert name part in 'lfn' from unicode to ASCII */ +#define CNV_THIS_PART(lfn) \ + ({ \ + unsigned char __part_uni[CHARS_PER_LFN*2]; \ + copy_lfn_part( __part_uni, lfn ); \ + cnv_unicode( __part_uni, CHARS_PER_LFN, 0 ); \ + }) + +/* Convert name parts collected so far (from previous slots) from unicode to + * ASCII */ +#define CNV_PARTS_SO_FAR() \ + (cnv_unicode( lfn_unicode+(lfn_slot*CHARS_PER_LFN*2), \ + lfn_parts*CHARS_PER_LFN, 0 )) + +/* This function converts an unicode string to a normal ASCII string, assuming + * ISO-8859-1 charset. Characters not in 8859-1 are converted to the same + * escape notation as used by the kernel, i.e. the uuencode-like ":xxx" */ +static char *cnv_unicode(const unsigned char *uni, int maxlen, int use_q) +{ + const unsigned char *up; + unsigned char *out, *cp; + int len, val; + + for (len = 0, up = uni; (up - uni) / 2 < maxlen && (up[0] || up[1]); + up += 2) { + if (UNICODE_CONVERTABLE(up[0], up[1])) + ++len; + else + len += 4; + } + cp = out = use_q ? qalloc(&mem_queue, len + 1) : alloc(len + 1); + + for (up = uni; (up - uni) / 2 < maxlen && (up[0] || up[1]); up += 2) { + if (UNICODE_CONVERTABLE(up[0], up[1])) + *cp++ = up[0]; + else { + /* here the same escape notation is used as in the Linux kernel */ + *cp++ = ':'; + val = (up[1] << 8) + up[0]; + cp[2] = fat_uni2esc[val & 0x3f]; + val >>= 6; + cp[1] = fat_uni2esc[val & 0x3f]; + val >>= 6; + cp[0] = fat_uni2esc[val & 0x3f]; + cp += 3; + } + } + *cp = 0; + + return (char *)out; +} + +static void copy_lfn_part(unsigned char *dst, LFN_ENT * lfn) +{ + memcpy(dst, lfn->name0_4, 10); + memcpy(dst + 10, lfn->name5_10, 12); + memcpy(dst + 22, lfn->name11_12, 4); +} + +static void clear_lfn_slots(int start, int end) +{ + int i; + LFN_ENT empty; + + /* New dir entry is zeroed except first byte, which is set to 0xe5. + * This is to avoid that some FAT-reading OSes (not Linux! ;) stop reading + * a directory at the first zero entry... + */ + memset(&empty, 0, sizeof(empty)); + empty.id = DELETED_FLAG; + + for (i = start; i <= end; ++i) { + fs_write(lfn_offsets[i], sizeof(LFN_ENT), &empty); + } +} + +void lfn_fix_checksum(loff_t from, loff_t to, const char *short_name) +{ + int i; + __u8 sum; + for (sum = 0, i = 0; i < 11; i++) + sum = (((sum & 1) << 7) | ((sum & 0xfe) >> 1)) + short_name[i]; + + for (; from < to; from += sizeof(LFN_ENT)) { + fs_write(from + offsetof(LFN_ENT, alias_checksum), sizeof(sum), &sum); + } +} + +void lfn_reset(void) +{ + if (lfn_unicode) + free(lfn_unicode); + lfn_unicode = NULL; + if (lfn_offsets) + free(lfn_offsets); + lfn_offsets = NULL; + lfn_slot = -1; +} + +/* This function is only called with de->attr == VFAT_LN_ATTR. It stores part + * of the long name. */ +void lfn_add_slot(DIR_ENT * de, loff_t dir_offset) +{ + LFN_ENT *lfn = (LFN_ENT *) de; + int slot = lfn->id & LFN_ID_SLOTMASK; + unsigned offset; + + if (lfn_slot == 0) + lfn_check_orphaned(); + + if (de->attr != VFAT_LN_ATTR) + die("lfn_add_slot called with non-LFN directory entry"); + + if (lfn->id & LFN_ID_START && slot != 0) { + if (lfn_slot != -1) { + int can_clear = 0; + /* There is already a LFN "in progess", so it is an error that a + * new start entry is here. */ + /* Causes: 1) if slot# == expected: start bit set mysteriously, 2) + * old LFN overwritten by new one */ + /* Fixes: 1) delete previous LFN 2) if slot# == expected and + * checksum ok: clear start bit */ + /* XXX: Should delay that until next LFN known (then can better + * display the name) */ + printf("A new long file name starts within an old one.\n"); + if (slot == lfn_slot && lfn->alias_checksum == lfn_checksum) { + char *part1 = CNV_THIS_PART(lfn); + char *part2 = CNV_PARTS_SO_FAR(); + printf(" It could be that the LFN start bit is wrong here\n" + " if \"%s\" seems to match \"%s\".\n", part1, part2); + free(part1); + free(part2); + can_clear = 1; + } + if (interactive) { + printf("1: Delete previous LFN\n2: Leave it as it is.\n"); + if (can_clear) + printf("3: Clear start bit and concatenate LFNs\n"); + } else + printf(" Not auto-correcting this.\n"); + if (interactive) { + switch (get_key(can_clear ? "123" : "12", "?")) { + case '1': + clear_lfn_slots(0, lfn_parts - 1); + lfn_reset(); + break; + case '2': + break; + case '3': + lfn->id &= ~LFN_ID_START; + fs_write(dir_offset + offsetof(LFN_ENT, id), + sizeof(lfn->id), &lfn->id); + break; + } + } + } + lfn_slot = slot; + lfn_checksum = lfn->alias_checksum; + lfn_unicode = alloc((lfn_slot * CHARS_PER_LFN + 1) * 2); + lfn_offsets = alloc(lfn_slot * sizeof(loff_t)); + lfn_parts = 0; + } else if (lfn_slot == -1 && slot != 0) { + /* No LFN in progress, but slot found; start bit missing */ + /* Causes: 1) start bit got lost, 2) Previous slot with start bit got + * lost */ + /* Fixes: 1) delete LFN, 2) set start bit */ + char *part = CNV_THIS_PART(lfn); + printf("Long filename fragment \"%s\" found outside a LFN " + "sequence.\n (Maybe the start bit is missing on the " + "last fragment)\n", part); + if (interactive) { + printf("1: Delete fragment\n2: Leave it as it is.\n" + "3: Set start bit\n"); + } else + printf(" Not auto-correcting this.\n"); + switch (interactive ? get_key("123", "?") : '2') { + case '1': + if (!lfn_offsets) + lfn_offsets = alloc(sizeof(loff_t)); + lfn_offsets[0] = dir_offset; + clear_lfn_slots(0, 0); + lfn_reset(); + return; + case '2': + lfn_reset(); + return; + case '3': + lfn->id |= LFN_ID_START; + fs_write(dir_offset + offsetof(LFN_ENT, id), + sizeof(lfn->id), &lfn->id); + lfn_slot = slot; + lfn_checksum = lfn->alias_checksum; + lfn_unicode = alloc((lfn_slot * CHARS_PER_LFN + 1) * 2); + lfn_offsets = alloc(lfn_slot * sizeof(loff_t)); + lfn_parts = 0; + break; + } + } else if (slot != lfn_slot) { + /* wrong sequence number */ + /* Causes: 1) seq-no destroyed */ + /* Fixes: 1) delete LFN, 2) fix number (maybe only if following parts + * are ok?, maybe only if checksum is ok?) (Attention: space + * for name was allocated before!) */ + int can_fix = 0; + printf("Unexpected long filename sequence number " + "(%d vs. expected %d).\n", slot, lfn_slot); + if (lfn->alias_checksum == lfn_checksum && lfn_slot > 0) { + char *part1 = CNV_THIS_PART(lfn); + char *part2 = CNV_PARTS_SO_FAR(); + printf(" It could be that just the number is wrong\n" + " if \"%s\" seems to match \"%s\".\n", part1, part2); + free(part1); + free(part2); + can_fix = 1; + } + if (interactive) { + printf + ("1: Delete LFN\n2: Leave it as it is (and ignore LFN so far)\n"); + if (can_fix) + printf("3: Correct sequence number\n"); + } else + printf(" Not auto-correcting this.\n"); + switch (interactive ? get_key(can_fix ? "123" : "12", "?") : '2') { + case '1': + if (!lfn_offsets) { + lfn_offsets = alloc(sizeof(loff_t)); + lfn_parts = 0; + } + lfn_offsets[lfn_parts++] = dir_offset; + clear_lfn_slots(0, lfn_parts - 1); + lfn_reset(); + return; + case '2': + lfn_reset(); + return; + case '3': + lfn->id = (lfn->id & ~LFN_ID_SLOTMASK) | lfn_slot; + fs_write(dir_offset + offsetof(LFN_ENT, id), + sizeof(lfn->id), &lfn->id); + break; + } + } + + if (lfn->alias_checksum != lfn_checksum) { + /* checksum mismatch */ + /* Causes: 1) checksum field here destroyed */ + /* Fixes: 1) delete LFN, 2) fix checksum */ + printf("Checksum in long filename part wrong " + "(%02x vs. expected %02x).\n", + lfn->alias_checksum, lfn_checksum); + if (interactive) { + printf("1: Delete LFN\n2: Leave it as it is.\n" + "3: Correct checksum\n"); + } else + printf(" Not auto-correcting this.\n"); + if (interactive) { + switch (get_key("123", "?")) { + case '1': + lfn_offsets[lfn_parts++] = dir_offset; + clear_lfn_slots(0, lfn_parts - 1); + lfn_reset(); + return; + case '2': + break; + case '3': + lfn->alias_checksum = lfn_checksum; + fs_write(dir_offset + offsetof(LFN_ENT, alias_checksum), + sizeof(lfn->alias_checksum), &lfn->alias_checksum); + break; + } + } + } + + if (lfn_slot != -1) { + lfn_slot--; + offset = lfn_slot * CHARS_PER_LFN * 2; + copy_lfn_part(lfn_unicode + offset, lfn); + if (lfn->id & LFN_ID_START) + lfn_unicode[offset + 26] = lfn_unicode[offset + 27] = 0; + lfn_offsets[lfn_parts++] = dir_offset; + } + + if (lfn->reserved != 0) { + printf("Reserved field in VFAT long filename slot is not 0 " + "(but 0x%02x).\n", lfn->reserved); + if (interactive) + printf("1: Fix.\n2: Leave it.\n"); + else + printf("Auto-setting to 0.\n"); + if (!interactive || get_key("12", "?") == '1') { + lfn->reserved = 0; + fs_write(dir_offset + offsetof(LFN_ENT, reserved), + sizeof(lfn->reserved), &lfn->reserved); + } + } + if (lfn->start != CT_LE_W(0)) { + printf("Start cluster field in VFAT long filename slot is not 0 " + "(but 0x%04x).\n", lfn->start); + if (interactive) + printf("1: Fix.\n2: Leave it.\n"); + else + printf("Auto-setting to 0.\n"); + if (!interactive || get_key("12", "?") == '1') { + lfn->start = CT_LE_W(0); + fs_write(dir_offset + offsetof(LFN_ENT, start), + sizeof(lfn->start), &lfn->start); + } + } +} + +/* This function is always called when de->attr != VFAT_LN_ATTR is found, to + * retrieve the previously constructed LFN. */ +char *lfn_get(DIR_ENT * de, loff_t * lfn_offset) +{ + char *lfn; + __u8 sum; + int i; + + *lfn_offset = 0; + if (de->attr == VFAT_LN_ATTR) + die("lfn_get called with LFN directory entry"); + +#if 0 + if (de->lcase) + printf("lcase=%02x\n", de->lcase); +#endif + + if (lfn_slot == -1) + /* no long name for this file */ + return NULL; + + if (lfn_slot != 0) { + /* The long name isn't finished yet. */ + /* Causes: 1) LFN slot overwritten by non-VFAT aware tool */ + /* Fixes: 1) delete LFN 2) move overwriting entry to somewhere else + * and let user enter missing part of LFN (hard to do :-() + * 3) renumber entries and truncate name */ + char *long_name = CNV_PARTS_SO_FAR(); + char *short_name = file_name(de->name); + printf("Unfinished long file name \"%s\".\n" + " (Start may have been overwritten by %s)\n", + long_name, short_name); + free(long_name); + if (interactive) { + printf("1: Delete LFN\n2: Leave it as it is.\n" + "3: Fix numbering (truncates long name and attaches " + "it to short name %s)\n", short_name); + } else + printf(" Not auto-correcting this.\n"); + switch (interactive ? get_key("123", "?") : '2') { + case '1': + clear_lfn_slots(0, lfn_parts - 1); + lfn_reset(); + return NULL; + case '2': + lfn_reset(); + return NULL; + case '3': + for (i = 0; i < lfn_parts; ++i) { + __u8 id = (lfn_parts - i) | (i == 0 ? LFN_ID_START : 0); + fs_write(lfn_offsets[i] + offsetof(LFN_ENT, id), + sizeof(id), &id); + } + memmove(lfn_unicode, lfn_unicode + lfn_slot * CHARS_PER_LFN * 2, + lfn_parts * CHARS_PER_LFN * 2); + break; + } + } + + for (sum = 0, i = 0; i < 11; i++) + sum = (((sum & 1) << 7) | ((sum & 0xfe) >> 1)) + de->name[i]; + if (sum != lfn_checksum) { + /* checksum doesn't match, long name doesn't apply to this alias */ + /* Causes: 1) alias renamed */ + /* Fixes: 1) Fix checksum in LFN entries */ + char *long_name = CNV_PARTS_SO_FAR(); + char *short_name = file_name(de->name); + printf("Wrong checksum for long file name \"%s\".\n" + " (Short name %s may have changed without updating the long name)\n", + long_name, short_name); + free(long_name); + if (interactive) { + printf("1: Delete LFN\n2: Leave it as it is.\n" + "3: Fix checksum (attaches to short name %s)\n", short_name); + } else + printf(" Not auto-correcting this.\n"); + if (interactive) { + switch (get_key("123", "?")) { + case '1': + clear_lfn_slots(0, lfn_parts - 1); + lfn_reset(); + return NULL; + case '2': + lfn_reset(); + return NULL; + case '3': + for (i = 0; i < lfn_parts; ++i) { + fs_write(lfn_offsets[i] + offsetof(LFN_ENT, alias_checksum), + sizeof(sum), &sum); + } + break; + } + } + } + + *lfn_offset = lfn_offsets[0]; + lfn = cnv_unicode(lfn_unicode, UNTIL_0, 1); + lfn_reset(); + return (lfn); +} + +void lfn_check_orphaned(void) +{ + char *long_name; + + if (lfn_slot == -1) + return; + + long_name = CNV_PARTS_SO_FAR(); + printf("Orphaned long file name part \"%s\"\n", long_name); + if (interactive) + printf("1: Delete.\n2: Leave it.\n"); + else + printf(" Auto-deleting.\n"); + if (!interactive || get_key("12", "?") == '1') { + clear_lfn_slots(0, lfn_parts - 1); + } + lfn_reset(); +} |