| package fat32 |
| |
| import ( |
| "errors" |
| "fmt" |
| "math" |
| "regexp" |
| "strings" |
| ) |
| |
| // By default, DOS names would be encoded as what Microsoft calls the OEM |
| // code page. This is however dependant on the code page settings of the |
| // OS reading the file name as it's not mentioned in FAT32 metadata. |
| // To get maximum compatibility and make it easy to read in hex editors |
| // this only encodes ASCII characters and not any specific code page. |
| // This can still result in garbled data when using a non-latin code page, |
| // but this is unavoidable. |
| // This is legal as there is no specific requirements for generating these |
| // DOS names and any semi-modern system should use the unicode filenames |
| // anyways. |
| |
| var invalidDOSNameChar = regexp.MustCompile("^[^A-Z0-9!#$%&'()@^_\x60{}~-]$") |
| |
| // validDOSName matches names which are valid and unique DOS 8.3 file names as |
| // well as valid ASCII |
| var validDOSName = regexp.MustCompile(`^^([A-Z0-9!#$%&'()@^_\x60{}~-]{0,8})(\.[A-Z0-9!#$%&'()-@^_\x60{}~-]{1,3})?$`) |
| |
| func makeUniqueDOSNames(inodes []*Inode) error { |
| taken := make(map[[11]byte]bool) |
| var lossyNameInodes []*Inode |
| // Make two passes to ensure that names can always be passed through even |
| // if they would conflict with a generated name. |
| for _, i := range inodes { |
| for j := range i.dosName { |
| i.dosName[j] = ' ' |
| } |
| nameUpper := strings.ToUpper(i.Name) |
| dosParts := validDOSName.FindStringSubmatch(nameUpper) |
| if dosParts != nil { |
| // Name is pass-through |
| copy(i.dosName[:8], []byte(dosParts[1])) |
| if len(dosParts[2]) > 0 { |
| // Skip the dot, it is implicit |
| copy(i.dosName[8:], []byte(dosParts[2][1:])) |
| } |
| if taken[i.dosName] { |
| // Mapping is unique, complain about the actual file name, not |
| // the 8.3 one |
| return fmt.Errorf("name %q occurs more than once in the same directory", i.Name) |
| } |
| taken[i.dosName] = true |
| continue |
| } |
| lossyNameInodes = append(lossyNameInodes, i) |
| } |
| // Willfully ignore the recommended short name generation algorithm as it |
| // requires tons of bookkeeping and doesn't result in stable names so |
| // cannot be relied on anyway. |
| // A FAT32 directory is limited to 2^16 entries (in practice less than half |
| // of that because of long file name entries), so 4 hex characters |
| // guarantee uniqueness, regardless of the rest of name. |
| var nameIdx int |
| for _, i := range lossyNameInodes { |
| nameUpper := strings.ToUpper(i.Name) |
| dotParts := strings.Split(nameUpper, ".") |
| for j := range dotParts { |
| // Remove all invalid chars |
| dotParts[j] = invalidDOSNameChar.ReplaceAllString(dotParts[j], "") |
| } |
| var fileName string |
| lastDotPart := dotParts[len(dotParts)-1] |
| if len(dotParts) > 1 && len(dotParts[0]) > 0 && len(lastDotPart) > 0 { |
| // We have a valid 8.3 extension |
| copy(i.dosName[8:], lastDotPart) |
| fileName = strings.Join(dotParts[:len(dotParts)-1], "") |
| } else { |
| fileName = strings.Join(dotParts[:], "") |
| } |
| copy(i.dosName[:4], fileName) |
| |
| for { |
| copy(i.dosName[4:], fmt.Sprintf("%04X", nameIdx)) |
| nameIdx++ |
| if nameIdx >= math.MaxUint16 { |
| return errors.New("invariant violated: unable to find unique name with 16 bit counter in 16 bit space") |
| } |
| if !taken[i.dosName] { |
| break |
| } |
| } |
| } |
| return nil |
| } |