Lorenz Brun | bd2ce6d | 2022-07-22 00:00:13 +0000 | [diff] [blame] | 1 | package fat32 |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "math" |
| 7 | "regexp" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | // By default, DOS names would be encoded as what Microsoft calls the OEM |
| 12 | // code page. This is however dependant on the code page settings of the |
| 13 | // OS reading the file name as it's not mentioned in FAT32 metadata. |
| 14 | // To get maximum compatibility and make it easy to read in hex editors |
| 15 | // this only encodes ASCII characters and not any specific code page. |
| 16 | // This can still result in garbled data when using a non-latin code page, |
| 17 | // but this is unavoidable. |
| 18 | // This is legal as there is no specific requirements for generating these |
| 19 | // DOS names and any semi-modern system should use the unicode filenames |
| 20 | // anyways. |
| 21 | |
| 22 | var invalidDOSNameChar = regexp.MustCompile("^[^A-Z0-9!#$%&'()@^_\x60{}~-]$") |
| 23 | |
| 24 | // validDOSName matches names which are valid and unique DOS 8.3 file names as |
| 25 | // well as valid ASCII |
| 26 | var validDOSName = regexp.MustCompile(`^^([A-Z0-9!#$%&'()@^_\x60{}~-]{0,8})(\.[A-Z0-9!#$%&'()-@^_\x60{}~-]{1,3})?$`) |
| 27 | |
| 28 | func makeUniqueDOSNames(inodes []*Inode) error { |
| 29 | taken := make(map[[11]byte]bool) |
| 30 | var lossyNameInodes []*Inode |
| 31 | // Make two passes to ensure that names can always be passed through even |
| 32 | // if they would conflict with a generated name. |
| 33 | for _, i := range inodes { |
| 34 | for j := range i.dosName { |
| 35 | i.dosName[j] = ' ' |
| 36 | } |
| 37 | nameUpper := strings.ToUpper(i.Name) |
| 38 | dosParts := validDOSName.FindStringSubmatch(nameUpper) |
| 39 | if dosParts != nil { |
| 40 | // Name is pass-through |
| 41 | copy(i.dosName[:8], []byte(dosParts[1])) |
| 42 | if len(dosParts[2]) > 0 { |
| 43 | // Skip the dot, it is implicit |
| 44 | copy(i.dosName[8:], []byte(dosParts[2][1:])) |
| 45 | } |
| 46 | if taken[i.dosName] { |
| 47 | // Mapping is unique, complain about the actual file name, not |
| 48 | // the 8.3 one |
| 49 | return fmt.Errorf("name %q occurs more than once in the same directory", i.Name) |
| 50 | } |
| 51 | taken[i.dosName] = true |
| 52 | continue |
| 53 | } |
| 54 | lossyNameInodes = append(lossyNameInodes, i) |
| 55 | } |
| 56 | // Willfully ignore the recommended short name generation algorithm as it |
| 57 | // requires tons of bookkeeping and doesn't result in stable names so |
| 58 | // cannot be relied on anyway. |
| 59 | // A FAT32 directory is limited to 2^16 entries (in practice less than half |
| 60 | // of that because of long file name entries), so 4 hex characters |
| 61 | // guarantee uniqueness, regardless of the rest of name. |
| 62 | var nameIdx int |
| 63 | for _, i := range lossyNameInodes { |
| 64 | nameUpper := strings.ToUpper(i.Name) |
| 65 | dotParts := strings.Split(nameUpper, ".") |
| 66 | for j := range dotParts { |
| 67 | // Remove all invalid chars |
| 68 | dotParts[j] = invalidDOSNameChar.ReplaceAllString(dotParts[j], "") |
| 69 | } |
| 70 | var fileName string |
| 71 | lastDotPart := dotParts[len(dotParts)-1] |
| 72 | if len(dotParts) > 1 && len(dotParts[0]) > 0 && len(lastDotPart) > 0 { |
| 73 | // We have a valid 8.3 extension |
| 74 | copy(i.dosName[8:], lastDotPart) |
| 75 | fileName = strings.Join(dotParts[:len(dotParts)-1], "") |
| 76 | } else { |
| 77 | fileName = strings.Join(dotParts[:], "") |
| 78 | } |
| 79 | copy(i.dosName[:4], fileName) |
| 80 | |
| 81 | for { |
| 82 | copy(i.dosName[4:], fmt.Sprintf("%04X", nameIdx)) |
| 83 | nameIdx++ |
| 84 | if nameIdx >= math.MaxUint16 { |
| 85 | return errors.New("invariant violated: unable to find unique name with 16 bit counter in 16 bit space") |
| 86 | } |
| 87 | if !taken[i.dosName] { |
| 88 | break |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | return nil |
| 93 | } |