171 lines
4.7 KiB
Go
171 lines
4.7 KiB
Go
|
|
package mft
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/binary"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"testing"
|
||
|
|
"unicode/utf16"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestShouldPreferFileName(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
current string
|
||
|
|
candidate string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{current: "", candidate: "LONGNAME.TXT", want: true},
|
||
|
|
{current: "PROGRA~1", candidate: "Program Files", want: true},
|
||
|
|
{current: "Program Files", candidate: "PROGRA~1", want: false},
|
||
|
|
{current: "abc", candidate: "abcdef", want: true},
|
||
|
|
{current: "abcdef", candidate: "abc", want: false},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
if got := shouldPreferFileName(tt.current, tt.candidate); got != tt.want {
|
||
|
|
t.Fatalf("shouldPreferFileName(%q, %q) = %v, want %v", tt.current, tt.candidate, got, tt.want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestShouldPreferFileNameWithNamespace(t *testing.T) {
|
||
|
|
if !shouldPreferFileNameWithNamespace("PROGRA~1", FileNameNamespaceDos, "Program Files", FileNameNamespaceWin32) {
|
||
|
|
t.Fatal("expected Win32 name to win over DOS name")
|
||
|
|
}
|
||
|
|
if shouldPreferFileNameWithNamespace("Program Files", FileNameNamespaceWin32, "PROGRA~1", FileNameNamespaceDos) {
|
||
|
|
t.Fatal("did not expect DOS name to replace Win32 name")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFileFromRecordIncludesParent(t *testing.T) {
|
||
|
|
parent := FileReference{RecordNumber: 42, SequenceNumber: 7}.ToUint64()
|
||
|
|
record := Record{
|
||
|
|
FileReference: FileReference{RecordNumber: 100, SequenceNumber: 9},
|
||
|
|
Flags: RecordFlagInUse,
|
||
|
|
Attributes: []Attribute{
|
||
|
|
{Type: AttributeTypeFileName, Data: testFileNameData("PROGRA~1", parent, FileNameNamespaceDos)},
|
||
|
|
{Type: AttributeTypeFileName, Data: testFileNameData("Program Files", parent, FileNameNamespaceWin32)},
|
||
|
|
{Type: AttributeTypeData, ActualSize: 12, AllocatedSize: 16},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
file, ok := FileFromRecord(record)
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("expected file to be extracted")
|
||
|
|
}
|
||
|
|
if file.Name != "Program Files" {
|
||
|
|
t.Fatalf("file.Name = %q, want %q", file.Name, "Program Files")
|
||
|
|
}
|
||
|
|
if file.Parent != parent {
|
||
|
|
t.Fatalf("file.Parent = %d, want %d", file.Parent, parent)
|
||
|
|
}
|
||
|
|
if file.Node != record.FileReference.ToUint64() {
|
||
|
|
t.Fatalf("file.Node = %d, want %d", file.Node, record.FileReference.ToUint64())
|
||
|
|
}
|
||
|
|
if file.Size != 12 || file.Aszie != 16 {
|
||
|
|
t.Fatalf("unexpected size fields: size=%d asize=%d", file.Size, file.Aszie)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCopyFilesReportsProgress(t *testing.T) {
|
||
|
|
progress := make([]float64, 0)
|
||
|
|
var dst testWriter
|
||
|
|
|
||
|
|
reader := &testChunkReader{chunks: [][]byte{{'a', 'b'}, {'c', 'd'}}}
|
||
|
|
written, err := copyFiles(&dst, reader, 4, func(_, _ int64, percent float64) {
|
||
|
|
progress = append(progress, percent)
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("copyFiles failed: %v", err)
|
||
|
|
}
|
||
|
|
if written != 4 {
|
||
|
|
t.Fatalf("written = %d, want 4", written)
|
||
|
|
}
|
||
|
|
if len(progress) == 0 {
|
||
|
|
t.Fatal("expected progress callbacks")
|
||
|
|
}
|
||
|
|
if progress[len(progress)-1] != 100 {
|
||
|
|
t.Fatalf("final progress = %v, want 100", progress[len(progress)-1])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWalkRecordsIgnoresPartialTail(t *testing.T) {
|
||
|
|
reader := bytes.NewReader(make([]byte, 2*defaultMFTRecordSize+17))
|
||
|
|
calls := 0
|
||
|
|
|
||
|
|
err := walkRecords(reader, int64(2*defaultMFTRecordSize+17), defaultMFTRecordSize, func(b []byte) (Record, error) {
|
||
|
|
calls++
|
||
|
|
if len(b) != int(defaultMFTRecordSize) {
|
||
|
|
t.Fatalf("parser got len=%d, want %d", len(b), defaultMFTRecordSize)
|
||
|
|
}
|
||
|
|
return Record{}, nil
|
||
|
|
}, func(Record) error {
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("walkRecords returned error: %v", err)
|
||
|
|
}
|
||
|
|
if calls != 2 {
|
||
|
|
t.Fatalf("parser calls = %d, want 2", calls)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWalkRecordsPropagatesVisitorError(t *testing.T) {
|
||
|
|
reader := bytes.NewReader(make([]byte, 2*defaultMFTRecordSize))
|
||
|
|
wantErr := errors.New("stop")
|
||
|
|
visited := 0
|
||
|
|
|
||
|
|
err := walkRecords(reader, int64(2*defaultMFTRecordSize), defaultMFTRecordSize, func([]byte) (Record, error) {
|
||
|
|
return Record{}, nil
|
||
|
|
}, func(Record) error {
|
||
|
|
visited++
|
||
|
|
if visited == 2 {
|
||
|
|
return wantErr
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
if !errors.Is(err, wantErr) {
|
||
|
|
t.Fatalf("walkRecords error = %v, want %v", err, wantErr)
|
||
|
|
}
|
||
|
|
if visited != 2 {
|
||
|
|
t.Fatalf("visited = %d, want 2", visited)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type testChunkReader struct {
|
||
|
|
chunks [][]byte
|
||
|
|
index int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *testChunkReader) Read(p []byte) (int, error) {
|
||
|
|
if r.index >= len(r.chunks) {
|
||
|
|
return 0, io.EOF
|
||
|
|
}
|
||
|
|
chunk := r.chunks[r.index]
|
||
|
|
r.index++
|
||
|
|
copy(p, chunk)
|
||
|
|
return len(chunk), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type testWriter struct {
|
||
|
|
wrote int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (w *testWriter) Write(p []byte) (int, error) {
|
||
|
|
w.wrote += len(p)
|
||
|
|
return len(p), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func testFileNameData(name string, parent uint64, namespace FileNameNamespace) []byte {
|
||
|
|
encoded := utf16.Encode([]rune(name))
|
||
|
|
data := make([]byte, 66+len(encoded)*2)
|
||
|
|
binary.LittleEndian.PutUint64(data[0:], parent)
|
||
|
|
data[0x40] = byte(len(encoded))
|
||
|
|
data[0x41] = byte(namespace)
|
||
|
|
for i, v := range encoded {
|
||
|
|
binary.LittleEndian.PutUint16(data[0x42+i*2:], v)
|
||
|
|
}
|
||
|
|
return data
|
||
|
|
}
|