blob: 49564683f6c6a039443fe0acf983ce5cce5644ca [file] [log] [blame]
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +00001package fat32
2
3import (
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
22var 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
26var validDOSName = regexp.MustCompile(`^^([A-Z0-9!#$%&'()@^_\x60{}~-]{0,8})(\.[A-Z0-9!#$%&'()-@^_\x60{}~-]{1,3})?$`)
27
28func 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}