You must login to view /gokrazy/tools/commit/8320e69cccc03429690bc86ee42fbc51ca821230.
The GitHub option should be usable for most people, it only links via username.

Files
internal/fat/reader.go
2021-06-21 18:25:16 +02:00

215 lines
5.3 KiB
Go

package fat
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"strings"
"time"
)
// Reader is a minimalistic FAT16B reader, which only aims to be
// compatible with file systems created by Writer.
type Reader struct {
r io.ReadSeeker
sectorSize uint16
sectorsPerCluster uint8
reservedSectors uint16
rootDirEntries uint16
fatSectors uint16
}
// NewReader creates a new FAT16B Reader by reading file system
// metadata.
func NewReader(r io.ReadSeeker) (*Reader, error) {
rd := &Reader{
r: r,
}
// Skip jumpCode and OEM
if _, err := r.Seek(3+8, io.SeekStart); err != nil {
return nil, err
}
if err := binary.Read(r, binary.LittleEndian, &rd.sectorSize); err != nil {
return nil, err
}
if err := binary.Read(r, binary.LittleEndian, &rd.sectorsPerCluster); err != nil {
return nil, err
}
if err := binary.Read(r, binary.LittleEndian, &rd.reservedSectors); err != nil {
return nil, err
}
// Skip number of FAT copies
if _, err := r.Seek(1, io.SeekCurrent); err != nil {
return nil, err
}
if err := binary.Read(r, binary.LittleEndian, &rd.rootDirEntries); err != nil {
return nil, err
}
// Skip number of sectors and media type
if _, err := r.Seek(2+1, io.SeekCurrent); err != nil {
return nil, err
}
if err := binary.Read(r, binary.LittleEndian, &rd.fatSectors); err != nil {
return nil, err
}
return rd, nil
}
func (r *Reader) fullSectors(bytes int64) int64 {
sectorSize := int64(r.sectorSize)
clusters := bytes / sectorSize
if bytes%sectorSize > 0 {
clusters++
}
return clusters
}
type dirEntry struct {
Name [8]byte
Ext [3]byte
Attr uint8
Reserved [10]byte
Time uint16
Date uint16
FirstCluster uint16
Size uint32
}
// Extents returns the offset and length of the file identified by path.
//
// This function is useful only on FAT file systems where all files
// are stored un-fragmented, such as file systems generated by Writer.
func (r *Reader) Extents(path string) (offset int64, length int64, err error) {
dirOffset := int64((r.reservedSectors + r.fatSectors)) * int64(r.sectorSize)
dataOffset := dirOffset + r.fullSectors(int64(r.rootDirEntries)*32)*int64(r.sectorSize)
numDirEntries := int(r.rootDirEntries)
components := strings.Split(path[1:], "/")
for _, component := range components {
for i := 0; i < numDirEntries; i++ {
if _, err := r.r.Seek(dirOffset+int64(i*32), io.SeekStart); err != nil {
return 0, 0, err
}
var entry dirEntry
if err := binary.Read(r.r, binary.LittleEndian, &entry); err != nil {
return 0, 0, err
}
// unused slot
if entry.Name[0] == 0 {
continue
}
var name string
if idx := bytes.IndexByte(entry.Name[:], ' '); idx > -1 {
name = string(entry.Name[:idx])
} else {
name = string(entry.Name[:])
}
if entry.Ext[0] != ' ' {
name += "." + string(entry.Ext[:])
}
// TODO: read long file names entries instead (with fallback for older installations)
primary, ext := shortFileName(component, make(map[string]bool))
shortName := strings.TrimSpace(primary)
if ext != "" {
shortName += "." + strings.TrimSpace(ext)
}
if strings.ToLower(name) != strings.ToLower(shortName) &&
name != component /* for backwards compatibility */ {
continue
}
offset := dataOffset + int64(entry.FirstCluster-2)*int64(r.sectorsPerCluster)*int64(r.sectorSize)
if entry.Attr == attrDirectory {
dirOffset = offset
break
}
return offset, int64(entry.Size), nil
}
}
return 0, 0, fmt.Errorf("%q not found", path)
}
func unmarshalTimeDate(t, d uint16) time.Time {
year := (d >> 9) & 0x7F
month := (d >> 5) & 0x0F
day := d & 0x1F
hour := (t >> 11) & 0x1F
minute := (t >> 5) & 0x3F
second := t & 0x1F
return time.Date(1980+int(year), time.Month(month), int(day), int(hour), int(minute), int(second)*2, 0, time.UTC)
}
// ModTime returns the modification time of the file identified by path.
//
// TODO: implement support for subdirectories
func (r *Reader) ModTime(path string) (time.Time, error) {
dirOffset := int64((r.reservedSectors + r.fatSectors)) * int64(r.sectorSize)
numDirEntries := int(r.rootDirEntries)
components := strings.Split(path[1:], "/")
for _, component := range components {
for i := 0; i < numDirEntries; i++ {
if _, err := r.r.Seek(dirOffset+int64(i*32), io.SeekStart); err != nil {
return time.Time{}, err
}
var entry dirEntry
if err := binary.Read(r.r, binary.LittleEndian, &entry); err != nil {
return time.Time{}, err
}
// unused slot
if entry.Name[0] == 0 {
continue
}
var name string
if idx := bytes.IndexByte(entry.Name[:], ' '); idx > -1 {
name = string(entry.Name[:idx])
} else {
name = string(entry.Name[:])
}
if entry.Ext[0] != ' ' {
name += "." + string(entry.Ext[:])
}
// TODO: read long file names entries instead (with fallback for older installations)
primary, ext := shortFileName(component, make(map[string]bool))
shortName := strings.TrimSpace(primary)
if ext != "" {
shortName += "." + strings.TrimSpace(ext)
}
if strings.ToLower(name) != strings.ToLower(shortName) &&
name != component /* for backwards compatibility */ {
continue
}
return unmarshalTimeDate(entry.Time, entry.Date), nil
}
}
return time.Time{}, fmt.Errorf("%q not found", path)
}