441 lines
11 KiB
Go
441 lines
11 KiB
Go
package fat
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf16"
|
|
|
|
"github.com/mitchellh/go-fs"
|
|
)
|
|
|
|
type DirectoryAttr uint8
|
|
|
|
const (
|
|
AttrReadOnly DirectoryAttr = 0x01
|
|
AttrHidden = 0x02
|
|
AttrSystem = 0x04
|
|
AttrVolumeId = 0x08
|
|
AttrDirectory = 0x10
|
|
AttrArchive = 0x20
|
|
AttrLongName = AttrReadOnly | AttrHidden | AttrSystem | AttrVolumeId
|
|
)
|
|
|
|
// The size in bytes of a single directory entry.
|
|
const DirectoryEntrySize = 32
|
|
|
|
// Mask applied to the ord of the last long entry.
|
|
const LastLongEntryMask = 0x40
|
|
|
|
// DirectoryCluster represents a cluster on the disk that contains
|
|
// entries/contents.
|
|
type DirectoryCluster struct {
|
|
entries []*DirectoryClusterEntry
|
|
fat16Root bool
|
|
startCluster uint32
|
|
}
|
|
|
|
// DirectoryClusterEntry is a single 32-byte entry that is part of the
|
|
// chain of entries in a directory cluster.
|
|
type DirectoryClusterEntry struct {
|
|
name string
|
|
ext string
|
|
attr DirectoryAttr
|
|
createTime time.Time
|
|
accessTime time.Time
|
|
writeTime time.Time
|
|
cluster uint32
|
|
fileSize uint32
|
|
deleted bool
|
|
|
|
longOrd uint8
|
|
longName string
|
|
longChecksum uint8
|
|
}
|
|
|
|
func DecodeDirectoryCluster(startCluster uint32, device fs.BlockDevice, fat *FAT) (*DirectoryCluster, error) {
|
|
bs := fat.bs
|
|
chain := fat.Chain(startCluster)
|
|
data := make([]byte, uint32(len(chain))*bs.BytesPerCluster())
|
|
for i, clusterNumber := range chain {
|
|
dataOffset := uint32(i) * bs.BytesPerCluster()
|
|
devOffset := int64(bs.ClusterOffset(int(clusterNumber)))
|
|
chainData := data[dataOffset : dataOffset+bs.BytesPerCluster()]
|
|
|
|
if _, err := device.ReadAt(chainData, devOffset); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
result, err := decodeDirectoryCluster(data, bs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result.startCluster = startCluster
|
|
return result, nil
|
|
}
|
|
|
|
// DecodeFAT16RootDirectory decodes the FAT16 root directory structure
|
|
// from the device.
|
|
func DecodeFAT16RootDirectoryCluster(device fs.BlockDevice, bs *BootSectorCommon) (*DirectoryCluster, error) {
|
|
data := make([]byte, DirectoryEntrySize*bs.RootEntryCount)
|
|
if _, err := device.ReadAt(data, int64(bs.RootDirOffset())); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := decodeDirectoryCluster(data, bs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result.fat16Root = true
|
|
return result, nil
|
|
}
|
|
|
|
func decodeDirectoryCluster(data []byte, bs *BootSectorCommon) (*DirectoryCluster, error) {
|
|
entries := make([]*DirectoryClusterEntry, 0, bs.RootEntryCount)
|
|
for i := uint16(0); i < uint16(len(data)/DirectoryEntrySize); i++ {
|
|
offset := i * DirectoryEntrySize
|
|
entryData := data[offset : offset+DirectoryEntrySize]
|
|
if entryData[0] == 0 {
|
|
break
|
|
}
|
|
|
|
entry, err := DecodeDirectoryClusterEntry(entryData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
result := &DirectoryCluster{
|
|
entries: entries,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func NewDirectoryCluster(start uint32, parent uint32, t time.Time) *DirectoryCluster {
|
|
cluster := new(DirectoryCluster)
|
|
cluster.startCluster = start
|
|
|
|
// Create the "." and ".." entries
|
|
cluster.entries = []*DirectoryClusterEntry{
|
|
{
|
|
accessTime: t,
|
|
attr: AttrDirectory,
|
|
cluster: start,
|
|
createTime: t,
|
|
name: ".",
|
|
writeTime: t,
|
|
},
|
|
{
|
|
accessTime: t,
|
|
attr: AttrDirectory,
|
|
cluster: parent,
|
|
createTime: t,
|
|
name: "..",
|
|
writeTime: t,
|
|
},
|
|
}
|
|
|
|
return cluster
|
|
}
|
|
|
|
// NewFat16RootDirectory creates a new DirectoryCluster that is meant only
|
|
// to be the root directory of a FAT12/FAT16 filesystem.
|
|
func NewFat16RootDirectoryCluster(bs *BootSectorCommon, label string) (*DirectoryCluster, error) {
|
|
if bs.RootEntryCount == 0 {
|
|
return nil, errors.New("root entry count is 0 in boot sector")
|
|
}
|
|
|
|
result := &DirectoryCluster{
|
|
entries: make([]*DirectoryClusterEntry, 1, bs.RootEntryCount),
|
|
}
|
|
|
|
// Create the volume ID entry
|
|
result.entries[0] = &DirectoryClusterEntry{
|
|
attr: AttrVolumeId,
|
|
name: label,
|
|
cluster: 0,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Bytes returns the on-disk byte data for this directory structure.
|
|
func (d *DirectoryCluster) Bytes() []byte {
|
|
result := make([]byte, cap(d.entries)*DirectoryEntrySize)
|
|
|
|
for i, entry := range d.entries {
|
|
offset := i * DirectoryEntrySize
|
|
entryBytes := entry.Bytes()
|
|
copy(result[offset:offset+DirectoryEntrySize], entryBytes)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// WriteToDevice writes the cluster to the device.
|
|
func (d *DirectoryCluster) WriteToDevice(device fs.BlockDevice, fat *FAT) error {
|
|
if d.fat16Root {
|
|
// Write the cluster to the FAT16 root directory location
|
|
offset := int64(fat.bs.RootDirOffset())
|
|
if _, err := device.WriteAt(d.Bytes(), offset); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
chain := &ClusterChain{
|
|
device: device,
|
|
fat: fat,
|
|
startCluster: d.startCluster,
|
|
}
|
|
|
|
if _, err := chain.Write(d.Bytes()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Bytes returns the on-disk byte data for this directory entry.
|
|
func (d *DirectoryClusterEntry) Bytes() []byte {
|
|
var result [DirectoryEntrySize]byte
|
|
|
|
if d.longName != "" {
|
|
runes := bytes.Runes([]byte(d.longName))
|
|
|
|
// The name must be zero-terminated then padded with 0xFF
|
|
// up to 13 characters
|
|
if len(runes) < 13 {
|
|
runes = append(runes, 0)
|
|
for len(runes) < 13 {
|
|
runes = append(runes, 0xFFFF)
|
|
}
|
|
}
|
|
|
|
// LDIR_Ord
|
|
result[0] = d.longOrd
|
|
|
|
// LDIR_Name1
|
|
for i := 0; i < int(math.Min(float64(len(runes)), 5)); i++ {
|
|
offset := 1 + (i * 2)
|
|
data := result[offset : offset+2]
|
|
binary.LittleEndian.PutUint16(data, uint16(runes[i]))
|
|
}
|
|
|
|
// LDIR_Attr
|
|
result[11] = byte(AttrLongName)
|
|
|
|
// LDIR_Type
|
|
result[12] = 0
|
|
|
|
// LDIR_Chksum
|
|
result[13] = d.longChecksum
|
|
|
|
// LDIR_Name2
|
|
for i := 0; i < 6; i++ {
|
|
offset := 14 + (i * 2)
|
|
data := result[offset : offset+2]
|
|
binary.LittleEndian.PutUint16(data, uint16(runes[i+5]))
|
|
}
|
|
|
|
// LDIR_FstClusLO
|
|
result[26] = 0
|
|
result[27] = 0
|
|
|
|
// LDIR_Name3
|
|
for i := 0; i < 2; i++ {
|
|
offset := 28 + (i * 2)
|
|
data := result[offset : offset+2]
|
|
binary.LittleEndian.PutUint16(data, uint16(runes[i+11]))
|
|
}
|
|
} else {
|
|
// DIR_Name
|
|
var simpleName string
|
|
if d.name == "." || d.name == ".." {
|
|
simpleName = d.name
|
|
} else {
|
|
simpleName = fmt.Sprintf("%s.%s", d.name, d.ext)
|
|
}
|
|
copy(result[0:11], shortNameEntryValue(simpleName))
|
|
|
|
// DIR_Attr
|
|
result[11] = byte(d.attr)
|
|
|
|
// DIR_CrtTime
|
|
crtDate, crtTime, crtTenths := encodeDOSTime(d.createTime)
|
|
result[13] = crtTenths
|
|
binary.LittleEndian.PutUint16(result[14:16], crtTime)
|
|
binary.LittleEndian.PutUint16(result[16:18], crtDate)
|
|
|
|
// DIR_LstAccDate
|
|
accDate, _, _ := encodeDOSTime(d.accessTime)
|
|
binary.LittleEndian.PutUint16(result[18:20], accDate)
|
|
|
|
// DIR_FstClusHI
|
|
binary.LittleEndian.PutUint16(result[20:22], uint16(d.cluster>>16))
|
|
|
|
// DIR_WrtTime and DIR_WrtDate
|
|
wrtDate, wrtTime, _ := encodeDOSTime(d.writeTime)
|
|
binary.LittleEndian.PutUint16(result[22:24], wrtTime)
|
|
binary.LittleEndian.PutUint16(result[24:26], wrtDate)
|
|
|
|
// DIR_FstClusLO
|
|
binary.LittleEndian.PutUint16(result[26:28], uint16(d.cluster&0xFFFF))
|
|
|
|
// DIR_FileSize
|
|
binary.LittleEndian.PutUint32(result[28:32], d.fileSize)
|
|
}
|
|
|
|
return result[:]
|
|
}
|
|
|
|
// IsLong returns true if this is a long entry.
|
|
func (d *DirectoryClusterEntry) IsLong() bool {
|
|
return (d.attr & AttrLongName) == AttrLongName
|
|
}
|
|
|
|
func (d *DirectoryClusterEntry) IsVolumeId() bool {
|
|
return (d.attr & AttrVolumeId) == AttrVolumeId
|
|
}
|
|
|
|
// DecodeDirectoryClusterEntry decodes a single directory entry in the
|
|
// Directory structure.
|
|
func DecodeDirectoryClusterEntry(data []byte) (*DirectoryClusterEntry, error) {
|
|
var result DirectoryClusterEntry
|
|
|
|
// Do the attributes so we can determine if we're dealing with long names
|
|
result.attr = DirectoryAttr(data[11])
|
|
if (result.attr & AttrLongName) == AttrLongName {
|
|
result.longOrd = data[0]
|
|
|
|
chars := make([]uint16, 13)
|
|
for i := 0; i < 5; i++ {
|
|
offset := 1 + (i * 2)
|
|
chars[i] = binary.LittleEndian.Uint16(data[offset : offset+2])
|
|
}
|
|
|
|
for i := 0; i < 6; i++ {
|
|
offset := 14 + (i * 2)
|
|
chars[i+5] = binary.LittleEndian.Uint16(data[offset : offset+2])
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
offset := 28 + (i * 2)
|
|
chars[i+11] = binary.LittleEndian.Uint16(data[offset : offset+2])
|
|
}
|
|
|
|
result.longName = string(utf16.Decode(chars))
|
|
result.longChecksum = data[13]
|
|
} else {
|
|
result.deleted = data[0] == 0xE5
|
|
|
|
// Basic attributes
|
|
if data[0] == 0x05 {
|
|
data[0] = 0xE5
|
|
}
|
|
|
|
result.name = strings.TrimRight(string(data[0:8]), " ")
|
|
result.ext = strings.TrimRight(string(data[8:11]), " ")
|
|
|
|
// Creation time
|
|
createTimeTenths := data[13]
|
|
createTimeWord := binary.LittleEndian.Uint16(data[14:16])
|
|
createDateWord := binary.LittleEndian.Uint16(data[16:18])
|
|
result.createTime = decodeDOSTime(createDateWord, createTimeWord, createTimeTenths)
|
|
|
|
// Access time
|
|
accessDateWord := binary.LittleEndian.Uint16(data[18:20])
|
|
result.accessTime = decodeDOSTime(accessDateWord, 0, 0)
|
|
|
|
// Write time
|
|
writeTimeWord := binary.LittleEndian.Uint16(data[22:24])
|
|
writeDateWord := binary.LittleEndian.Uint16(data[24:26])
|
|
result.writeTime = decodeDOSTime(writeDateWord, writeTimeWord, 0)
|
|
|
|
// Cluster
|
|
result.cluster = uint32(binary.LittleEndian.Uint16(data[20:22]))
|
|
result.cluster <<= 4
|
|
result.cluster |= uint32(binary.LittleEndian.Uint16(data[26:28]))
|
|
|
|
// File size
|
|
result.fileSize = binary.LittleEndian.Uint32(data[28:32])
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// NewLongDirectoryClusterEntry returns the series of directory cluster
|
|
// entries that need to be written for a long directory entry. This list
|
|
// of entries does NOT contain the short name entry.
|
|
func NewLongDirectoryClusterEntry(name string, shortName string) ([]*DirectoryClusterEntry, error) {
|
|
// Split up the shortName properly
|
|
checksum := checksumShortName(shortNameEntryValue(shortName))
|
|
|
|
// Calcualte the number of entries we'll actually need to store
|
|
// the long name.
|
|
numLongEntries := len(name) / 13
|
|
if len(name)%13 != 0 {
|
|
numLongEntries++
|
|
}
|
|
|
|
entries := make([]*DirectoryClusterEntry, numLongEntries)
|
|
for i := 0; i < numLongEntries; i++ {
|
|
entries[i] = new(DirectoryClusterEntry)
|
|
entry := entries[i]
|
|
entry.attr = AttrLongName
|
|
entry.longOrd = uint8(numLongEntries - i)
|
|
|
|
if i == 0 {
|
|
entry.longOrd |= LastLongEntryMask
|
|
}
|
|
|
|
// Calculate the offsets of the string for this entry
|
|
j := (numLongEntries - i - 1) * 13
|
|
k := j + 13
|
|
if k > len(name) {
|
|
k = len(name)
|
|
}
|
|
|
|
entry.longChecksum = checksum
|
|
entry.longName = name[j:k]
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
func decodeDOSTime(date, dosTime uint16, tenths uint8) time.Time {
|
|
return time.Date(
|
|
1980+int(date>>9),
|
|
time.Month((date>>5)&0x0F),
|
|
int(date&0x1F),
|
|
int(dosTime>>11),
|
|
int((dosTime>>5)&0x3F),
|
|
int((dosTime&0x1F)*2),
|
|
int(tenths)*10*int(time.Millisecond),
|
|
time.Local)
|
|
}
|
|
|
|
func encodeDOSTime(t time.Time) (uint16, uint16, uint8) {
|
|
var date uint16 = uint16((t.Year() - 1980) << 9)
|
|
date |= uint16(t.Month()) << 5
|
|
date += uint16(t.Day() & 0xFF)
|
|
|
|
var time uint16 = uint16(t.Hour() << 11)
|
|
time |= uint16(t.Minute() << 5)
|
|
time += uint16(t.Second() / 2)
|
|
|
|
var tenths uint8
|
|
// TODO(mitchellh): Do tenths
|
|
|
|
return date, time, tenths
|
|
}
|