完善 Windows 运维封装与 NTFS 索引解析

- 新增自启动幂等配置、统一错误语义、进程等待和进程树终止能力
- 增强服务生命周期管理,支持等待状态、重启、幂等创建和配置更新
- 新增 NTFS 卷索引、文件 ID 解析、文件遍历、USN 变更监听和 bookmark 持久化
- 修复 NTFS boot sector、fragment、MFT、USN 解析边界和路径重建问题
- 补充权限、进程、服务、NTFS 解析和工作流回归测试
- 增加 Windows 测试脚本和管理员 NTFS smoke 验证脚本
- 升级 Go 兼容版本到 1.18,并更新 stario、win32api 及相关间接依赖
This commit is contained in:
2026-06-09 15:59:31 +08:00
parent feb1a21da8
commit 7e6cc73106
31 changed files with 4937 additions and 981 deletions
+4 -9
View File
@@ -1,6 +1,6 @@
/*
Package bootsect provides functions to parse the boot sector (also sometimes called Volume Boot Record, VBR, or
$Boot file) of an NTFS volume.
Package bootsect provides functions to parse the boot sector (also sometimes called Volume Boot Record, VBR, or
$Boot file) of an NTFS volume.
*/
package bootsect
@@ -35,12 +35,7 @@ func Parse(data []byte) (BootSector, error) {
}
r := binutil.NewLittleEndianReader(data)
bytesPerSector := int(r.Uint16(0x0B))
sectorsPerCluster := int(int8(r.Byte(0x0D)))
if sectorsPerCluster < 0 {
// Quoth Wikipedia: The number of sectors in a cluster. If the value is negative, the amount of sectors is 2
// to the power of the absolute value of this field.
sectorsPerCluster = 1 << -sectorsPerCluster
}
sectorsPerCluster := int(r.Byte(0x0D))
bytesPerCluster := bytesPerSector * sectorsPerCluster
return BootSector{
OemId: string(r.Read(0x03, 8)),
@@ -49,7 +44,7 @@ func Parse(data []byte) (BootSector, error) {
MediaDescriptor: r.Byte(0x15),
SectorsPerTrack: int(r.Uint16(0x18)),
NumberofHeads: int(r.Uint16(0x1A)),
HiddenSectors: int(r.Uint16(0x1C)),
HiddenSectors: int(r.Uint32(0x1C)),
TotalSectors: r.Uint64(0x28),
MftClusterNumber: r.Uint64(0x30),
MftMirrorClusterNumber: r.Uint64(0x38),
+41
View File
@@ -0,0 +1,41 @@
package bootsect
import (
"encoding/binary"
"testing"
)
func TestParseBootSectorUsesCorrectFieldWidths(t *testing.T) {
data := make([]byte, 512)
copy(data[0x03:], []byte("NTFS "))
binary.LittleEndian.PutUint16(data[0x0B:], 512)
data[0x0D] = 8
data[0x15] = 0xF8
binary.LittleEndian.PutUint16(data[0x18:], 63)
binary.LittleEndian.PutUint16(data[0x1A:], 255)
binary.LittleEndian.PutUint32(data[0x1C:], 0x11223344)
binary.LittleEndian.PutUint64(data[0x28:], 0x0102030405060708)
binary.LittleEndian.PutUint64(data[0x30:], 0x1112131415161718)
binary.LittleEndian.PutUint64(data[0x38:], 0x2122232425262728)
data[0x40] = 0xF6
data[0x44] = 1
copy(data[0x48:], []byte{1, 2, 3, 4, 5, 6, 7, 8})
boot, err := Parse(data)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
if boot.SectorsPerCluster != 8 {
t.Fatalf("SectorsPerCluster = %d, want 8", boot.SectorsPerCluster)
}
if boot.HiddenSectors != 0x11223344 {
t.Fatalf("HiddenSectors = %#x, want %#x", boot.HiddenSectors, 0x11223344)
}
if boot.FileRecordSegmentSizeInBytes != 1024 {
t.Fatalf("FileRecordSegmentSizeInBytes = %d, want 1024", boot.FileRecordSegmentSizeInBytes)
}
if boot.IndexBufferSizeInBytes != 4096 {
t.Fatalf("IndexBufferSizeInBytes = %d, want 4096", boot.IndexBufferSizeInBytes)
}
}
+2 -1
View File
@@ -17,10 +17,11 @@ import (
)
func main() {
f, size, err := mft.GetMFTFile(`C:\`)
f, size, err := mft.GetMFTFileReader(`C:\`)
if err != nil {
panic(err)
}
defer f.Close()
recordSize := int64(1024)
i := int64(0)
fmt.Println("start size is", size)
+26 -15
View File
@@ -1,25 +1,24 @@
/*
Package fragment contains a Reader which can read Fragments which may be scattered around a volume (and perhaps even
not in sequence). Typically these could be translated from MFT attribute DataRuns. To convert MFT attribute DataRuns
to Fragments for use in the fragment Reader, use mft.DataRunsToFragments().
Package fragment contains a Reader which can read Fragments which may be scattered around a volume (and perhaps even
not in sequence). Typically these could be translated from MFT attribute DataRuns. To convert MFT attribute DataRuns
to Fragments for use in the fragment Reader, use mft.DataRunsToFragments().
Implementation notes
# Implementation notes
When the fragment Reader is near the end of a fragment and a Read() call requests more data than what is left in
the current fragment, the Reader will exhaust only the current fragment and return that data (which could be less
than len(p)). A next Read() call will then seek to the next fragment and continue reading there. When the last
fragment is exhausted by a Read(), it will return the remaining bytes read and a nil error. Any subsequent Read()
calls after that will return 0, io.EOF.
When the fragment Reader is near the end of a fragment and a Read() call requests more data than what is left in
the current fragment, the Reader will exhaust only the current fragment and return that data (which could be less
than len(p)). A next Read() call will then seek to the next fragment and continue reading there. When the last
fragment is exhausted by a Read(), it will return the remaining bytes read and a nil error. Any subsequent Read()
calls after that will return 0, io.EOF.
When accessing a new fragment, the Reader will seek using the absolute Length in the fragment from the start
of the contained io.ReadSeeker (using io.SeekStart).
When accessing a new fragment, the Reader will seek using the absolute Length in the fragment from the start
of the contained io.ReadSeeker (using io.SeekStart).
*/
package fragment
import (
"fmt"
"io"
"os"
)
// Fragment contains an absolute Offset in bytes from the start of a volume and a Length of the fragment, also in bytes.
@@ -33,22 +32,25 @@ type Fragment struct {
// fragment has been exhaused, each subsequent Read() will return io.EOF.
type Reader struct {
src io.ReadSeeker
closer io.Closer
fragments []Fragment
idx int
remaining int64
file *os.File
}
// NewReader initializes a new Reader from the io.ReaderSeeker and fragments and returns a pointer to. Note that
// fragments may not be sequential in order, so the io.ReadSeeker should support seeking backwards (or rather, from the
// start).
func NewReader(src io.ReadSeeker, fragments []Fragment) *Reader {
return &Reader{src: src, fragments: fragments, idx: -1, remaining: 0}
r := &Reader{src: src, fragments: fragments, idx: -1, remaining: 0}
if closer, ok := src.(io.Closer); ok {
r.closer = closer
}
return r
}
func (r *Reader) Read(p []byte) (n int, err error) {
if r.idx >= len(r.fragments) {
r.src.(*os.File).Close()
return 0, io.EOF
}
@@ -81,3 +83,12 @@ func (r *Reader) Read(p []byte) (n int, err error) {
r.remaining -= int64(n)
return n, err
}
func (r *Reader) Close() error {
if r.closer == nil {
return nil
}
err := r.closer.Close()
r.closer = nil
return err
}
+61
View File
@@ -0,0 +1,61 @@
package fragment
import (
"bytes"
"io"
"testing"
)
type readSeekCloser struct {
*bytes.Reader
closed bool
}
func (r *readSeekCloser) Close() error {
r.closed = true
return nil
}
func TestReaderReadsFragmentsWithoutOwningEOF(t *testing.T) {
reader := NewReader(bytes.NewReader([]byte("abcdef")), []Fragment{
{Offset: 1, Length: 2},
{Offset: 4, Length: 2},
})
buf := make([]byte, 4)
n, err := reader.Read(buf)
if err != nil {
t.Fatalf("first read failed: %v", err)
}
if got := string(buf[:n]); got != "bc" {
t.Fatalf("first read = %q, want %q", got, "bc")
}
n, err = reader.Read(buf)
if err != nil {
t.Fatalf("second read failed: %v", err)
}
if got := string(buf[:n]); got != "ef" {
t.Fatalf("second read = %q, want %q", got, "ef")
}
n, err = reader.Read(buf)
if err != io.EOF {
t.Fatalf("third read error = %v, want %v", err, io.EOF)
}
if n != 0 {
t.Fatalf("third read count = %d, want 0", n)
}
}
func TestReaderCloseClosesUnderlyingCloser(t *testing.T) {
src := &readSeekCloser{Reader: bytes.NewReader([]byte("abcdef"))}
reader := NewReader(src, []Fragment{{Offset: 0, Length: 2}})
if err := reader.Close(); err != nil {
t.Fatalf("Close failed: %v", err)
}
if !src.closed {
t.Fatal("underlying closer was not closed")
}
}
+197 -110
View File
@@ -9,8 +9,14 @@ import (
"b612.me/wincmd/ntfs/utf16"
)
var (
reallyStrangeEpoch = time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)
const (
minStandardInformationLength = 48
minFileNameLength = 66
minAttributeListEntryLength = 26
minIndexRootLength = 32
minIndexEntryLength = 13
indexRootHeaderLength = 16
indexRootEntryOffset = 0x20
)
// StandardInformation represents the data contained in a $STANDARD_INFORMATION attribute.
@@ -33,27 +39,12 @@ type StandardInformation struct {
// AttributeTypeStandardInformation) into StandardInformation. Note that no additional correctness checks are done, so
// it's up to the caller to ensure the passed data actually represents a $STANDARD_INFORMATION attribute's data.
func ParseStandardInformation(b []byte) (StandardInformation, error) {
if len(b) < 48 {
return StandardInformation{}, fmt.Errorf("expected at least %d bytes but got %d", 48, len(b))
if len(b) < minStandardInformationLength {
return StandardInformation{}, fmt.Errorf("expected at least %d bytes but got %d", minStandardInformationLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
ownerId := uint32(0)
securityId := uint32(0)
quotaCharged := uint64(0)
updateSequenceNumber := uint64(0)
if len(b) >= 0x30+4 {
ownerId = r.Uint32(0x30)
}
if len(b) >= 0x34+4 {
securityId = r.Uint32(0x34)
}
if len(b) >= 0x38+8 {
quotaCharged = r.Uint64(0x38)
}
if len(b) >= 0x40+8 {
updateSequenceNumber = r.Uint64(0x40)
}
ownerId, securityId, quotaCharged, updateSequenceNumber := parseStandardInformationTail(r, len(b))
return StandardInformation{
Creation: ConvertFileTime(r.Uint64(0x00)),
FileLastModified: ConvertFileTime(r.Uint64(0x08)),
@@ -70,6 +61,22 @@ func ParseStandardInformation(b []byte) (StandardInformation, error) {
}, nil
}
func parseStandardInformationTail(r *binutil.BinReader, length int) (ownerID uint32, securityID uint32, quotaCharged uint64, updateSequenceNumber uint64) {
if length >= 0x30+4 {
ownerID = r.Uint32(0x30)
}
if length >= 0x34+4 {
securityID = r.Uint32(0x34)
}
if length >= 0x38+8 {
quotaCharged = r.Uint64(0x38)
}
if length >= 0x40+8 {
updateSequenceNumber = r.Uint64(0x40)
}
return ownerID, securityID, quotaCharged, updateSequenceNumber
}
// FileAttribute represents a bit mask of various file attributes.
type FileAttribute uint32
@@ -84,7 +91,7 @@ const (
FileAttributeTemporary FileAttribute = 0x0100
FileAttributeSparseFile FileAttribute = 0x0200
FileAttributeReparsePoint FileAttribute = 0x0400
FileAttributeCompressed FileAttribute = 0x1000
FileAttributeCompressed FileAttribute = 0x0800
FileAttributeOffline FileAttribute = 0x1000
FileAttributeNotContentIndexed FileAttribute = 0x2000
FileAttributeEncrypted FileAttribute = 0x4000
@@ -127,12 +134,12 @@ type FileName struct {
// no additional correctness checks are done, so it's up to the caller to ensure the passed data actually represents a
// $FILE_NAME attribute's data.
func ParseFileName(b []byte) (FileName, error) {
if len(b) < 66 {
return FileName{}, fmt.Errorf("expected at least %d bytes but got %d", 66, len(b))
if len(b) < minFileNameLength {
return FileName{}, fmt.Errorf("expected at least %d bytes but got %d", minFileNameLength, len(b))
}
fileNameLength := int(b[0x40 : 0x40+1][0]) * 2
minExpectedSize := 66 + fileNameLength
minExpectedSize := minFileNameLength + fileNameLength
if len(b) < minExpectedSize {
return FileName{}, fmt.Errorf("expected at least %d bytes but got %d", minExpectedSize, len(b))
}
@@ -172,41 +179,69 @@ type AttributeListEntry struct {
// list of AttributeListEntry. Note that no additional correctness checks are done, so it's up to the caller to ensure
// the passed data actually represents a $ATTRIBUTE_LIST attribute's data.
func ParseAttributeList(b []byte) ([]AttributeListEntry, error) {
if len(b) < 26 {
return []AttributeListEntry{}, fmt.Errorf("expected at least %d bytes but got %d", 26, len(b))
if len(b) < minAttributeListEntryLength {
return []AttributeListEntry{}, fmt.Errorf("expected at least %d bytes but got %d", minAttributeListEntryLength, len(b))
}
entries := make([]AttributeListEntry, 0)
for len(b) > 0 {
r := binutil.NewLittleEndianReader(b)
entryLength := int(r.Uint16(0x04))
if len(b) < entryLength {
return entries, fmt.Errorf("expected at least %d bytes remaining for AttributeList entry but is %d", entryLength, len(b))
}
nameLength := int(r.Byte(0x06))
name := ""
if nameLength != 0 {
nameOffset := int(r.Byte(0x07))
name = utf16.DecodeString(r.Read(nameOffset, nameLength*2), binary.LittleEndian)
}
baseRef, err := ParseFileReference(r.Read(0x10, 8))
entry, entryLength, err := parseAttributeListEntry(b)
if err != nil {
return entries, fmt.Errorf("unable to parse base record reference: %v", err)
}
entry := AttributeListEntry{
Type: AttributeType(r.Uint32(0)),
Name: name,
StartingVCN: r.Uint64(0x08),
BaseRecordReference: baseRef,
AttributeId: r.Uint16(0x18),
return entries, err
}
entries = append(entries, entry)
b = r.ReadFrom(entryLength)
b = b[entryLength:]
}
return entries, nil
}
func parseAttributeListEntry(b []byte) (AttributeListEntry, int, error) {
if len(b) < minAttributeListEntryLength {
return AttributeListEntry{}, 0, fmt.Errorf("expected at least %d bytes but got %d", minAttributeListEntryLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
entryLength := int(r.Uint16(0x04))
if entryLength < minAttributeListEntryLength {
return AttributeListEntry{}, 0, fmt.Errorf("attribute list entry length %d is smaller than minimum %d", entryLength, minAttributeListEntryLength)
}
if len(b) < entryLength {
return AttributeListEntry{}, 0, fmt.Errorf("expected at least %d bytes remaining for AttributeList entry but is %d", entryLength, len(b))
}
name, err := parseAttributeListEntryName(r, b, entryLength)
if err != nil {
return AttributeListEntry{}, 0, err
}
baseRef, err := ParseFileReference(r.Read(0x10, 8))
if err != nil {
return AttributeListEntry{}, 0, fmt.Errorf("unable to parse base record reference: %v", err)
}
return AttributeListEntry{
Type: AttributeType(r.Uint32(0)),
Name: name,
StartingVCN: r.Uint64(0x08),
BaseRecordReference: baseRef,
AttributeId: r.Uint16(0x18),
}, entryLength, nil
}
func parseAttributeListEntryName(r *binutil.BinReader, b []byte, entryLength int) (string, error) {
nameLength := int(r.Byte(0x06))
if nameLength == 0 {
return "", nil
}
nameOffset := int(r.Byte(0x07))
nameEnd := nameOffset + nameLength*2
if nameEnd > entryLength || nameEnd > len(b) {
return "", fmt.Errorf("attribute list entry name exceeds entry boundary: offset=%d length=%d entry=%d", nameOffset, nameLength*2, entryLength)
}
return utf16.DecodeString(r.Read(nameOffset, nameLength*2), binary.LittleEndian), nil
}
// CollationType indicates how the entries in an index should be ordered.
type CollationType uint32
@@ -246,98 +281,150 @@ type IndexEntry struct {
// IndexRoot. Note that no additional correctness checks are done, so it's up to the caller to ensure the passed data
// actually represents a $INDEX_ROOT attribute's data.
func ParseIndexRoot(b []byte) (IndexRoot, error) {
if len(b) < 32 {
return IndexRoot{}, fmt.Errorf("expected at least %d bytes but got %d", 32, len(b))
}
r := binutil.NewLittleEndianReader(b)
attributeType := AttributeType(r.Uint32(0x00))
if attributeType != AttributeTypeFileName {
return IndexRoot{}, fmt.Errorf("unable to handle attribute type %d (%s) in $INDEX_ROOT", attributeType, attributeType.Name())
}
uTotalSize := r.Uint32(0x14)
if int64(uTotalSize) > maxInt {
return IndexRoot{}, fmt.Errorf("index root size %d overflows maximum int value %d", uTotalSize, maxInt)
}
totalSize := int(uTotalSize)
expectedSize := totalSize + 16
if len(b) < expectedSize {
return IndexRoot{}, fmt.Errorf("expected %d bytes in $INDEX_ROOT but is %d", expectedSize, len(b))
header, entryData, err := parseIndexRootHeader(b)
if err != nil {
return IndexRoot{}, err
}
entries := []IndexEntry{}
if totalSize >= 16 {
parsed, err := parseIndexEntries(r.Read(0x20, totalSize-16))
if len(entryData) > 0 {
parsed, err := parseIndexEntries(entryData)
if err != nil {
return IndexRoot{}, fmt.Errorf("error parsing index entries: %v", err)
}
entries = parsed
}
return IndexRoot{
AttributeType: header.AttributeType,
CollationType: header.CollationType,
BytesPerRecord: header.BytesPerRecord,
ClustersPerRecord: header.ClustersPerRecord,
Flags: header.Flags,
Entries: entries,
}, nil
}
func parseIndexRootHeader(b []byte) (IndexRoot, []byte, error) {
if len(b) < minIndexRootLength {
return IndexRoot{}, nil, fmt.Errorf("expected at least %d bytes but got %d", minIndexRootLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
attributeType := AttributeType(r.Uint32(0x00))
if attributeType != AttributeTypeFileName {
return IndexRoot{}, nil, fmt.Errorf("unable to handle attribute type %d (%s) in $INDEX_ROOT", attributeType, attributeType.Name())
}
uTotalSize := r.Uint32(0x14)
if int64(uTotalSize) > maxInt {
return IndexRoot{}, nil, fmt.Errorf("index root size %d overflows maximum int value %d", uTotalSize, maxInt)
}
totalSize := int(uTotalSize)
expectedSize := totalSize + indexRootHeaderLength
if len(b) < expectedSize {
return IndexRoot{}, nil, fmt.Errorf("expected %d bytes in $INDEX_ROOT but is %d", expectedSize, len(b))
}
entryData := []byte{}
if totalSize >= indexRootHeaderLength {
entryData = r.Read(indexRootEntryOffset, totalSize-indexRootHeaderLength)
}
return IndexRoot{
AttributeType: attributeType,
CollationType: CollationType(r.Uint32(0x04)),
BytesPerRecord: r.Uint32(0x08),
ClustersPerRecord: r.Uint32(0x0C),
Flags: r.Uint32(0x1C),
Entries: entries,
}, nil
}, entryData, nil
}
func parseIndexEntries(b []byte) ([]IndexEntry, error) {
if len(b) < 13 {
return []IndexEntry{}, fmt.Errorf("expected at least %d bytes but got %d", 13, len(b))
if len(b) < minIndexEntryLength {
return []IndexEntry{}, fmt.Errorf("expected at least %d bytes but got %d", minIndexEntryLength, len(b))
}
entries := make([]IndexEntry, 0)
for len(b) > 0 {
r := binutil.NewLittleEndianReader(b)
entryLength := int(r.Uint16(0x08))
if len(b) < entryLength {
return entries, fmt.Errorf("index entry length indicates %d bytes but got %d", entryLength, len(b))
}
flags := r.Uint32(0x0C)
pointsToSubNode := flags&0b1 != 0
isLastEntryInNode := flags&0b10 != 0
contentLength := int(r.Uint16(0x0A))
fileName := FileName{}
if contentLength != 0 && !isLastEntryInNode {
parsedFileName, err := ParseFileName(r.Read(0x10, contentLength))
if err != nil {
return entries, fmt.Errorf("error parsing $FILE_NAME record in index entry: %v", err)
}
fileName = parsedFileName
}
subNodeVcn := uint64(0)
if pointsToSubNode {
subNodeVcn = r.Uint64(entryLength - 8)
}
fileReference, err := ParseFileReference(r.Read(0x00, 8))
entry, entryLength, err := parseIndexEntry(b)
if err != nil {
return entries, fmt.Errorf("unable to file reference: %v", err)
}
entry := IndexEntry{
FileReference: fileReference,
Flags: flags,
FileName: fileName,
SubNodeVCN: subNodeVcn,
return entries, err
}
entries = append(entries, entry)
b = r.ReadFrom(entryLength)
b = b[entryLength:]
}
return entries, nil
}
func parseIndexEntry(b []byte) (IndexEntry, int, error) {
if len(b) < minIndexEntryLength {
return IndexEntry{}, 0, fmt.Errorf("expected at least %d bytes but got %d", minIndexEntryLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
entryLength := int(r.Uint16(0x08))
if entryLength < minIndexEntryLength {
return IndexEntry{}, 0, fmt.Errorf("index entry length %d is smaller than minimum %d", entryLength, minIndexEntryLength)
}
if len(b) < entryLength {
return IndexEntry{}, 0, fmt.Errorf("index entry length indicates %d bytes but got %d", entryLength, len(b))
}
flags := r.Uint32(0x0C)
contentLength := int(r.Uint16(0x0A))
fileName, err := parseIndexEntryFileName(r, b, entryLength, contentLength, flags)
if err != nil {
return IndexEntry{}, 0, err
}
subNodeVcn, err := parseIndexEntrySubNodeVCN(r, entryLength, flags)
if err != nil {
return IndexEntry{}, 0, err
}
fileReference, err := ParseFileReference(r.Read(0x00, 8))
if err != nil {
return IndexEntry{}, 0, fmt.Errorf("unable to file reference: %v", err)
}
return IndexEntry{
FileReference: fileReference,
Flags: flags,
FileName: fileName,
SubNodeVCN: subNodeVcn,
}, entryLength, nil
}
func parseIndexEntryFileName(r *binutil.BinReader, b []byte, entryLength int, contentLength int, flags uint32) (FileName, error) {
isLastEntryInNode := flags&0b10 != 0
if contentLength == 0 || isLastEntryInNode {
return FileName{}, nil
}
contentEnd := 0x10 + contentLength
if contentEnd > entryLength || contentEnd > len(b) {
return FileName{}, fmt.Errorf("index entry content exceeds entry boundary: content=%d entry=%d", contentLength, entryLength)
}
fileName, err := ParseFileName(r.Read(0x10, contentLength))
if err != nil {
return FileName{}, fmt.Errorf("error parsing $FILE_NAME record in index entry: %v", err)
}
return fileName, nil
}
func parseIndexEntrySubNodeVCN(r *binutil.BinReader, entryLength int, flags uint32) (uint64, error) {
pointsToSubNode := flags&0b1 != 0
if !pointsToSubNode {
return 0, nil
}
if entryLength < 8 {
return 0, fmt.Errorf("index entry length %d is too small for sub-node VCN", entryLength)
}
return r.Uint64(entryLength - 8), nil
}
// ConvertFileTime converts a Windows "file time" to a time.Time. A "file time" is a 64-bit value that represents the
// number of 100-nanosecond intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal Time
// (UTC). See also: https://docs.microsoft.com/en-us/windows/win32/sysinfo/file-times
func ConvertFileTime(timeValue uint64) time.Time {
dur := time.Duration(int64(timeValue))
r := time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC)
for i := 0; i < 100; i++ {
r = r.Add(dur)
}
return r
const ticksPerSecond = uint64(10000000)
const unixOffsetSeconds = int64(-11644473600)
seconds := int64(timeValue / ticksPerSecond)
nanoseconds := int64(timeValue%ticksPerSecond) * 100
return time.Unix(unixOffsetSeconds+seconds, nanoseconds).UTC()
}
+137
View File
@@ -0,0 +1,137 @@
package mft
import (
"encoding/binary"
"testing"
"time"
"unicode/utf16"
)
func TestConvertFileTime(t *testing.T) {
tests := []struct {
name string
value uint64
want time.Time
}{
{
name: "epoch",
value: 0,
want: time.Date(1601, time.January, 1, 0, 0, 0, 0, time.UTC),
},
{
name: "one second",
value: 10000000,
want: time.Date(1601, time.January, 1, 0, 0, 1, 0, time.UTC),
},
}
for _, tt := range tests {
got := ConvertFileTime(tt.value)
if !got.Equal(tt.want) {
t.Fatalf("%s: ConvertFileTime(%d) = %v, want %v", tt.name, tt.value, got, tt.want)
}
}
}
func TestFileAttributeConstants(t *testing.T) {
if FileAttributeCompressed == FileAttributeOffline {
t.Fatal("FileAttributeCompressed and FileAttributeOffline should differ")
}
}
func TestParseAttributeListParsesNamedEntry(t *testing.T) {
baseRef := FileReference{RecordNumber: 33, SequenceNumber: 2}
entries, err := ParseAttributeList(buildAttributeListEntry(AttributeTypeData, "alt", 7, baseRef, 9))
if err != nil {
t.Fatalf("ParseAttributeList returned error: %v", err)
}
if len(entries) != 1 {
t.Fatalf("len(entries) = %d, want 1", len(entries))
}
if entries[0].Name != "alt" {
t.Fatalf("entries[0].Name = %q, want %q", entries[0].Name, "alt")
}
if entries[0].BaseRecordReference != baseRef {
t.Fatalf("entries[0].BaseRecordReference = %+v, want %+v", entries[0].BaseRecordReference, baseRef)
}
if entries[0].StartingVCN != 7 {
t.Fatalf("entries[0].StartingVCN = %d, want 7", entries[0].StartingVCN)
}
}
func TestParseIndexRootParsesSingleEntry(t *testing.T) {
fileRef := FileReference{RecordNumber: 51, SequenceNumber: 4}
fileNameData := testFileNameData("hello.txt", FileReference{RecordNumber: 9, SequenceNumber: 1}.ToUint64(), FileNameNamespaceWin32)
root, err := ParseIndexRoot(buildIndexRoot(buildIndexEntry(fileRef, fileNameData, 0, 0)))
if err != nil {
t.Fatalf("ParseIndexRoot returned error: %v", err)
}
if len(root.Entries) != 1 {
t.Fatalf("len(root.Entries) = %d, want 1", len(root.Entries))
}
if root.Entries[0].FileReference != fileRef {
t.Fatalf("root.Entries[0].FileReference = %+v, want %+v", root.Entries[0].FileReference, fileRef)
}
if root.Entries[0].FileName.Name != "hello.txt" {
t.Fatalf("root.Entries[0].FileName.Name = %q, want %q", root.Entries[0].FileName.Name, "hello.txt")
}
}
func buildAttributeListEntry(attrType AttributeType, name string, startingVCN uint64, baseRef FileReference, attrID uint16) []byte {
encodedName := utf16.Encode([]rune(name))
entryLength := minAttributeListEntryLength + len(encodedName)*2
buf := make([]byte, entryLength)
binary.LittleEndian.PutUint32(buf[0x00:], uint32(attrType))
binary.LittleEndian.PutUint16(buf[0x04:], uint16(entryLength))
buf[0x06] = byte(len(encodedName))
if len(encodedName) > 0 {
buf[0x07] = 0x1A
for i, v := range encodedName {
binary.LittleEndian.PutUint16(buf[0x1A+i*2:], v)
}
}
binary.LittleEndian.PutUint64(buf[0x08:], startingVCN)
copy(buf[0x10:], encodeRawFileReference(baseRef))
binary.LittleEndian.PutUint16(buf[0x18:], attrID)
return buf
}
func buildIndexEntry(fileRef FileReference, fileNameData []byte, flags uint32, subNodeVCN uint64) []byte {
entryLength := 0x10 + len(fileNameData)
if flags&0b1 != 0 {
entryLength += 8
}
buf := make([]byte, entryLength)
copy(buf[0x00:], encodeRawFileReference(fileRef))
binary.LittleEndian.PutUint16(buf[0x08:], uint16(entryLength))
binary.LittleEndian.PutUint16(buf[0x0A:], uint16(len(fileNameData)))
binary.LittleEndian.PutUint32(buf[0x0C:], flags)
copy(buf[0x10:], fileNameData)
if flags&0b1 != 0 {
binary.LittleEndian.PutUint64(buf[entryLength-8:], subNodeVCN)
}
return buf
}
func buildIndexRoot(entry []byte) []byte {
totalSize := indexRootHeaderLength + len(entry)
buf := make([]byte, indexRootEntryOffset+len(entry))
binary.LittleEndian.PutUint32(buf[0x00:], uint32(AttributeTypeFileName))
binary.LittleEndian.PutUint32(buf[0x04:], uint32(CollationTypeFileName))
binary.LittleEndian.PutUint32(buf[0x08:], 4096)
binary.LittleEndian.PutUint32(buf[0x0C:], 1)
binary.LittleEndian.PutUint32(buf[0x10:], 0x10)
binary.LittleEndian.PutUint32(buf[0x14:], uint32(totalSize))
binary.LittleEndian.PutUint32(buf[0x18:], uint32(totalSize))
copy(buf[indexRootEntryOffset:], entry)
return buf
}
func encodeRawFileReference(ref FileReference) []byte {
buf := make([]byte, 8)
rawRecord := make([]byte, 8)
binary.LittleEndian.PutUint64(rawRecord, ref.RecordNumber)
copy(buf[:6], rawRecord[:6])
binary.LittleEndian.PutUint16(buf[6:], ref.SequenceNumber)
return buf
}
+279 -150
View File
@@ -1,14 +1,15 @@
/*
Package mft provides functions to parse records and their attributes in an NTFS Master File Table ("MFT" for short).
Package mft provides functions to parse records and their attributes in an NTFS Master File Table ("MFT" for short).
Basic usage
# Basic usage
First parse a record using mft.ParseRecord(), which parses the record header and the attribute headers. Then parse
each attribute's data individually using the various mft.Parse...() functions.
// Error handling left out for brevity
record, err := mft.ParseRecord()
attrs, err := record.FindAttributes(mft.AttributeTypeFileName)
fileName, err := mft.ParseFileName(attrs[0])
First parse a record using mft.ParseRecord(), which parses the record header and the attribute headers. Then parse
each attribute's data individually using the various mft.Parse...() functions.
// Error handling left out for brevity
record, err := mft.ParseRecord()
attrs, err := record.FindAttributes(mft.AttributeTypeFileName)
fileName, err := mft.ParseFileName(attrs[0])
*/
package mft
@@ -26,7 +27,42 @@ var (
fileSignature = []byte{0x46, 0x49, 0x4c, 0x45}
)
const maxInt = int64(^uint(0) >> 1)
const (
maxInt = int64(^uint(0) >> 1)
minRecordHeaderLength = 42
minAttributeDataLength = 22
minAttributeListHeader = 8
minAttributeTypeLength = 4
dataRunTerminatorLength = 1
)
type recordHeader struct {
signature []byte
fileReference FileReference
baseRecordReference FileReference
logFileSequence uint64
hardLinkCount int
flags RecordFlag
actualSize uint32
allocatedSize uint32
nextAttributeID int
firstAttributeOffset int
}
type attributeHeader struct {
attrType AttributeType
resident bool
name string
flags AttributeFlags
attributeID int
payloadOffset int
}
type attributePayload struct {
allocatedSize uint64
actualSize uint64
data []byte
}
// A Record represents an MFT entry, excluding all technical data (such as "offset to first attribute"). The Attributes
// list only contains the attribute headers and raw data; the attribute data has to be parsed separately. When this is a
@@ -48,51 +84,68 @@ type Record struct {
// ParseRecord parses bytes into a Record after applying fixup. The data is assumed to be in Little Endian order. Only
// the attribute headers are parsed, not the actual attribute data.
func ParseRecord(b []byte) (Record, error) {
if len(b) < 42 {
return Record{}, fmt.Errorf("record data length should be at least 42 but is %d", len(b))
}
sig := b[:4]
if bytes.Compare(sig, fileSignature) != 0 {
return Record{}, fmt.Errorf("unknown record signature: %# x", sig)
}
b = binutil.Duplicate(b)
r := binutil.NewLittleEndianReader(b)
baseRecordRef, err := ParseFileReference(r.Read(0x20, 8))
header, data, err := parseRecordHeader(b)
if err != nil {
return Record{}, fmt.Errorf("unable to parse base record reference: %v", err)
return Record{}, err
}
firstAttributeOffset := int(r.Uint16(0x14))
if firstAttributeOffset < 0 || firstAttributeOffset >= len(b) {
return Record{}, fmt.Errorf("invalid first attribute offset %d (data length: %d)", firstAttributeOffset, len(b))
}
updateSequenceOffset := int(r.Uint16(0x04))
updateSequenceSize := int(r.Uint16(0x06))
b, err = applyFixUp(b, updateSequenceOffset, updateSequenceSize)
if err != nil {
return Record{}, fmt.Errorf("unable to apply fixup: %v", err)
}
attributes, err := ParseAttributes(b[firstAttributeOffset:])
attributes, err := ParseAttributes(data[header.firstAttributeOffset:])
if err != nil {
return Record{}, err
}
return Record{
Signature: binutil.Duplicate(sig),
FileReference: FileReference{RecordNumber: uint64(r.Uint32(0x2C)), SequenceNumber: r.Uint16(0x10)},
BaseRecordReference: baseRecordRef,
LogFileSequenceNumber: r.Uint64(0x08),
HardLinkCount: int(r.Uint16(0x12)),
Flags: RecordFlag(r.Uint16(0x16)),
ActualSize: r.Uint32(0x18),
AllocatedSize: r.Uint32(0x1C),
NextAttributeId: int(r.Uint16(0x28)),
Signature: header.signature,
FileReference: header.fileReference,
BaseRecordReference: header.baseRecordReference,
LogFileSequenceNumber: header.logFileSequence,
HardLinkCount: header.hardLinkCount,
Flags: header.flags,
ActualSize: header.actualSize,
AllocatedSize: header.allocatedSize,
NextAttributeId: header.nextAttributeID,
Attributes: attributes,
}, nil
}
func parseRecordHeader(b []byte) (recordHeader, []byte, error) {
if len(b) < minRecordHeaderLength {
return recordHeader{}, nil, fmt.Errorf("record data length should be at least %d but is %d", minRecordHeaderLength, len(b))
}
if !bytes.Equal(b[:4], fileSignature) {
return recordHeader{}, nil, fmt.Errorf("unknown record signature: %# x", b[:4])
}
data := binutil.Duplicate(b)
r := binutil.NewLittleEndianReader(data)
baseRecordRef, err := ParseFileReference(r.Read(0x20, 8))
if err != nil {
return recordHeader{}, nil, fmt.Errorf("unable to parse base record reference: %v", err)
}
firstAttributeOffset := int(r.Uint16(0x14))
if firstAttributeOffset < 0 || firstAttributeOffset >= len(data) {
return recordHeader{}, nil, fmt.Errorf("invalid first attribute offset %d (data length: %d)", firstAttributeOffset, len(data))
}
if _, err := applyFixUp(data, int(r.Uint16(0x04)), int(r.Uint16(0x06))); err != nil {
return recordHeader{}, nil, fmt.Errorf("unable to apply fixup: %v", err)
}
return recordHeader{
signature: binutil.Duplicate(data[:4]),
fileReference: FileReference{RecordNumber: uint64(r.Uint32(0x2C)), SequenceNumber: r.Uint16(0x10)},
baseRecordReference: baseRecordRef,
logFileSequence: r.Uint64(0x08),
hardLinkCount: int(r.Uint16(0x12)),
flags: RecordFlag(r.Uint16(0x16)),
actualSize: r.Uint32(0x18),
allocatedSize: r.Uint32(0x1C),
nextAttributeID: int(r.Uint16(0x28)),
firstAttributeOffset: firstAttributeOffset,
}, data, nil
}
// A FileReference represents a reference to an MFT record. Since the FileReference in a Record is only 4 bytes, the
// RecordNumber will probably not exceed 32 bits.
type FileReference struct {
@@ -102,10 +155,8 @@ type FileReference struct {
func (f FileReference) ToUint64() uint64 {
origin := make([]byte, 8)
binary.LittleEndian.PutUint16(origin, f.SequenceNumber)
origin[6] = origin[0]
origin[7] = origin[1]
binary.LittleEndian.PutUint32(origin, uint32(f.RecordNumber))
binary.LittleEndian.PutUint64(origin, f.RecordNumber)
binary.LittleEndian.PutUint16(origin[6:], f.SequenceNumber)
return binary.LittleEndian.Uint64(origin)
}
@@ -117,7 +168,7 @@ func ParseFileReference(b []byte) (FileReference, error) {
}
return FileReference{
RecordNumber: binary.LittleEndian.Uint64(padTo(b[:6], 8)),
RecordNumber: binary.LittleEndian.Uint64(padToUnsigned(b[:6], 8)),
SequenceNumber: binary.LittleEndian.Uint16(b[6:]),
}, nil
}
@@ -139,19 +190,45 @@ func (f *RecordFlag) Is(c RecordFlag) bool {
}
func applyFixUp(b []byte, offset int, length int) ([]byte, error) {
if offset < 0 {
return nil, fmt.Errorf("update sequence offset %d is negative", offset)
}
if length < 2 {
return nil, fmt.Errorf("update sequence length %d is too small", length)
}
updateSequenceLength := length * 2
if offset > len(b) || updateSequenceLength > len(b)-offset {
return nil, fmt.Errorf("update sequence range [%d:%d] exceeds record length %d", offset, offset+updateSequenceLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
updateSequence := r.Read(offset, length*2) // length is in pairs, not bytes
updateSequence := r.Read(offset, updateSequenceLength) // length is in pairs, not bytes
updateSequenceNumber := updateSequence[:2]
updateSequenceArray := updateSequence[2:]
if len(updateSequenceArray) == 0 || len(updateSequenceArray)%2 != 0 {
return nil, fmt.Errorf("invalid update sequence array length %d", len(updateSequenceArray))
}
sectorCount := len(updateSequenceArray) / 2
if sectorCount == 0 {
return nil, fmt.Errorf("update sequence does not contain any sector entries")
}
if len(b)%sectorCount != 0 {
return nil, fmt.Errorf("record length %d is not divisible by sector count %d", len(b), sectorCount)
}
sectorSize := len(b) / sectorCount
if sectorSize < 2 {
return nil, fmt.Errorf("invalid sector size %d", sectorSize)
}
for i := 1; i <= sectorCount; i++ {
offset := sectorSize*i - 2
if bytes.Compare(updateSequenceNumber, b[offset:offset+2]) != 0 {
return nil, fmt.Errorf("update sequence mismatch at pos %d", offset)
sectorOffset := sectorSize*i - 2
if sectorOffset < 0 || sectorOffset+2 > len(b) {
return nil, fmt.Errorf("invalid sector offset %d for record length %d", sectorOffset, len(b))
}
if !bytes.Equal(updateSequenceNumber, b[sectorOffset:sectorOffset+2]) {
return nil, fmt.Errorf("update sequence mismatch at pos %d", sectorOffset)
}
}
@@ -237,99 +314,129 @@ func ParseAttributes(b []byte) ([]Attribute, error) {
}
attributes := make([]Attribute, 0)
for len(b) > 0 {
if len(b) < 4 {
return nil, fmt.Errorf("attribute header data should be at least 4 bytes but is %d", len(b))
recordData, remaining, done, err := nextAttributeRecordData(b)
if err != nil {
return nil, err
}
r := binutil.NewLittleEndianReader(b)
attrType := r.Uint32(0)
if attrType == uint32(AttributeTypeTerminator) {
if done {
break
}
if len(b) < 8 {
return nil, fmt.Errorf("cannot read attribute header record length, data should be at least 8 bytes but is %d", len(b))
}
uRecordLength := r.Uint32(0x04)
if int64(uRecordLength) > maxInt {
return nil, fmt.Errorf("record length %d overflows maximum int value %d", uRecordLength, maxInt)
}
recordLength := int(uRecordLength)
if recordLength <= 0 {
return nil, fmt.Errorf("cannot handle attribute with zero or negative record length %d", recordLength)
}
if recordLength > len(b) {
return nil, fmt.Errorf("attribute record length %d exceeds data length %d", recordLength, len(b))
}
recordData := r.Read(0, recordLength)
attribute, err := ParseAttribute(recordData)
if err != nil {
return nil, err
}
attributes = append(attributes, attribute)
b = r.ReadFrom(recordLength)
b = remaining
}
return attributes, nil
}
func nextAttributeRecordData(b []byte) (recordData []byte, remaining []byte, done bool, err error) {
if len(b) < minAttributeTypeLength {
return nil, nil, false, fmt.Errorf("attribute header data should be at least %d bytes but is %d", minAttributeTypeLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
if AttributeType(r.Uint32(0)) == AttributeTypeTerminator {
return nil, nil, true, nil
}
if len(b) < minAttributeListHeader {
return nil, nil, false, fmt.Errorf("cannot read attribute header record length, data should be at least %d bytes but is %d", minAttributeListHeader, len(b))
}
uRecordLength := r.Uint32(0x04)
if int64(uRecordLength) > maxInt {
return nil, nil, false, fmt.Errorf("record length %d overflows maximum int value %d", uRecordLength, maxInt)
}
recordLength := int(uRecordLength)
if recordLength <= 0 {
return nil, nil, false, fmt.Errorf("cannot handle attribute with zero or negative record length %d", recordLength)
}
if recordLength > len(b) {
return nil, nil, false, fmt.Errorf("attribute record length %d exceeds data length %d", recordLength, len(b))
}
return r.Read(0, recordLength), r.ReadFrom(recordLength), false, nil
}
// ParseAttribute parses bytes into an Attribute. The data is assumed to be in Little Endian order. Only the attribute
// headers are parsed, not the actual attribute data.
func ParseAttribute(b []byte) (Attribute, error) {
if len(b) < 22 {
return Attribute{}, fmt.Errorf("attribute data should be at least 22 bytes but is %d", len(b))
if len(b) < minAttributeDataLength {
return Attribute{}, fmt.Errorf("attribute data should be at least %d bytes but is %d", minAttributeDataLength, len(b))
}
r := binutil.NewLittleEndianReader(b)
nameLength := r.Byte(0x09)
nameOffset := r.Uint16(0x0A)
name := ""
if nameLength != 0 {
nameBytes := r.Read(int(nameOffset), int(nameLength)*2)
name = utf16.DecodeString(nameBytes, binary.LittleEndian)
header, err := parseAttributeHeader(r, b)
if err != nil {
return Attribute{}, err
}
resident := r.Byte(0x08) == 0x00
var attributeData []byte
actualSize := uint64(0)
allocatedSize := uint64(0)
if resident {
dataOffset := int(r.Uint16(0x14))
uDataLength := r.Uint32(0x10)
if int64(uDataLength) > maxInt {
return Attribute{}, fmt.Errorf("attribute data length %d overflows maximum int value %d", uDataLength, maxInt)
}
dataLength := int(uDataLength)
expectedDataLength := dataOffset + dataLength
if len(b) < expectedDataLength {
return Attribute{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", expectedDataLength, len(b))
}
attributeData = r.Read(dataOffset, dataLength)
} else {
dataOffset := int(r.Uint16(0x20))
if len(b) < dataOffset {
return Attribute{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", dataOffset, len(b))
}
allocatedSize = r.Uint64(0x28)
actualSize = r.Uint64(0x30)
attributeData = r.ReadFrom(int(dataOffset))
payload, err := parseAttributePayload(r, b, header)
if err != nil {
return Attribute{}, err
}
return Attribute{
Type: AttributeType(r.Uint32(0)),
Resident: resident,
Name: name,
Flags: AttributeFlags(r.Uint16(0x0C)),
AttributeId: int(r.Uint16(0x0E)),
AllocatedSize: allocatedSize,
ActualSize: actualSize,
Data: binutil.Duplicate(attributeData),
Type: header.attrType,
Resident: header.resident,
Name: header.name,
Flags: header.flags,
AttributeId: header.attributeID,
AllocatedSize: payload.allocatedSize,
ActualSize: payload.actualSize,
Data: binutil.Duplicate(payload.data),
}, nil
}
func parseAttributeHeader(r *binutil.BinReader, b []byte) (attributeHeader, error) {
nameLength := int(r.Byte(0x09))
nameOffset := int(r.Uint16(0x0A))
name := ""
if nameLength != 0 {
nameEnd := nameOffset + nameLength*2
if len(b) < nameEnd {
return attributeHeader{}, fmt.Errorf("expected attribute name length to be at least %d but is %d", nameEnd, len(b))
}
name = utf16.DecodeString(r.Read(nameOffset, nameLength*2), binary.LittleEndian)
}
resident := r.Byte(0x08) == 0x00
payloadOffset := int(r.Uint16(0x20))
if resident {
payloadOffset = int(r.Uint16(0x14))
}
return attributeHeader{
attrType: AttributeType(r.Uint32(0)),
resident: resident,
name: name,
flags: AttributeFlags(r.Uint16(0x0C)),
attributeID: int(r.Uint16(0x0E)),
payloadOffset: payloadOffset,
}, nil
}
func parseAttributePayload(r *binutil.BinReader, b []byte, header attributeHeader) (attributePayload, error) {
if header.resident {
uDataLength := r.Uint32(0x10)
if int64(uDataLength) > maxInt {
return attributePayload{}, fmt.Errorf("attribute data length %d overflows maximum int value %d", uDataLength, maxInt)
}
dataLength := int(uDataLength)
expectedDataLength := header.payloadOffset + dataLength
if len(b) < expectedDataLength {
return attributePayload{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", expectedDataLength, len(b))
}
return attributePayload{data: r.Read(header.payloadOffset, dataLength)}, nil
}
if len(b) < header.payloadOffset {
return attributePayload{}, fmt.Errorf("expected attribute data length to be at least %d but is %d", header.payloadOffset, len(b))
}
return attributePayload{
allocatedSize: r.Uint64(0x28),
actualSize: r.Uint64(0x30),
data: r.ReadFrom(header.payloadOffset),
}, nil
}
@@ -350,38 +457,45 @@ func ParseDataRuns(b []byte) ([]DataRun, error) {
runs := make([]DataRun, 0)
for len(b) > 0 {
r := binutil.NewLittleEndianReader(b)
header := r.Byte(0)
if header == 0 {
run, consumed, done, err := parseDataRun(b)
if err != nil {
return nil, err
}
if done {
break
}
lengthLength := int(header &^ 0xF0)
offsetLength := int(header >> 4)
dataRunDataLength := offsetLength + lengthLength
headerAndDataLength := dataRunDataLength + 1
if len(b) < headerAndDataLength {
return nil, fmt.Errorf("expected at least %d bytes of datarun data but is %d", headerAndDataLength, len(b))
}
dataRunData := r.Reader(1, dataRunDataLength)
lengthBytes := dataRunData.Read(0, lengthLength)
dataLength := binary.LittleEndian.Uint64(padTo(lengthBytes, 8))
offsetBytes := dataRunData.Read(lengthLength, offsetLength)
dataOffset := int64(binary.LittleEndian.Uint64(padTo(offsetBytes, 8)))
runs = append(runs, DataRun{OffsetCluster: dataOffset, LengthInClusters: dataLength})
b = r.ReadFrom(headerAndDataLength)
runs = append(runs, run)
b = b[consumed:]
}
return runs, nil
}
func parseDataRun(b []byte) (DataRun, int, bool, error) {
r := binutil.NewLittleEndianReader(b)
header := r.Byte(0)
if header == 0 {
return DataRun{}, dataRunTerminatorLength, true, nil
}
lengthLength := int(header &^ 0xF0)
offsetLength := int(header >> 4)
dataRunDataLength := offsetLength + lengthLength
headerAndDataLength := dataRunDataLength + dataRunTerminatorLength
if len(b) < headerAndDataLength {
return DataRun{}, 0, false, fmt.Errorf("expected at least %d bytes of datarun data but is %d", headerAndDataLength, len(b))
}
dataRunData := r.Reader(1, dataRunDataLength)
lengthBytes := dataRunData.Read(0, lengthLength)
offsetBytes := dataRunData.Read(lengthLength, offsetLength)
return DataRun{
OffsetCluster: int64(binary.LittleEndian.Uint64(padToSigned(offsetBytes, 8))),
LengthInClusters: binary.LittleEndian.Uint64(padToUnsigned(lengthBytes, 8)),
}, headerAndDataLength, false, nil
}
// DataRunsToFragments transform a list of DataRuns with relative offsets and lengths specified in cluster into a list
// of fragment.Fragment elements with absolute offsets and lengths specified in bytes (for example for use in a
// fragment.Reader). Note that data will probably not align to a cluster exactly so there could be some padding at the
@@ -401,7 +515,7 @@ func DataRunsToFragments(runs []DataRun, bytesPerCluster int) []fragment.Fragmen
return frags
}
func padTo(data []byte, length int) []byte {
func padToUnsigned(data []byte, length int) []byte {
if len(data) > length {
return data
}
@@ -413,7 +527,22 @@ func padTo(data []byte, length int) []byte {
return result
}
copy(result, data)
if data[len(data)-1]&0b10000000 == 0b10000000 {
return result
}
func padToSigned(data []byte, length int) []byte {
if len(data) > length {
return data
}
if len(data) == length {
return data
}
result := make([]byte, length)
if len(data) == 0 {
return result
}
copy(result, data)
if data[len(data)-1]&0x80 != 0 {
for i := len(data); i < length; i++ {
result[i] = 0xFF
}
+98
View File
@@ -0,0 +1,98 @@
package mft
import (
"encoding/binary"
"testing"
)
func TestParseAttributeRejectsShortNameData(t *testing.T) {
data := make([]byte, minAttributeDataLength)
binary.LittleEndian.PutUint32(data[0x00:], uint32(AttributeTypeData))
data[0x08] = 0x00
data[0x09] = 2
binary.LittleEndian.PutUint16(data[0x0A:], 0x15)
if _, err := ParseAttribute(data); err == nil {
t.Fatal("expected ParseAttribute to reject truncated attribute name")
}
}
func TestParseDataRunsRejectsShortRecord(t *testing.T) {
if _, err := ParseDataRuns([]byte{0x11, 0x01}); err == nil {
t.Fatal("expected ParseDataRuns to reject truncated data run")
}
}
func TestFileReferenceRoundTripPreservesHighRecordBits(t *testing.T) {
want := FileReference{
RecordNumber: 0x00000000BA987654,
SequenceNumber: 0x1234,
}
encoded := make([]byte, 8)
binary.LittleEndian.PutUint64(encoded, want.RecordNumber)
binary.LittleEndian.PutUint16(encoded[6:], want.SequenceNumber)
got, err := ParseFileReference(encoded)
if err != nil {
t.Fatalf("ParseFileReference returned error: %v", err)
}
if got != want {
t.Fatalf("ParseFileReference = %+v, want %+v", got, want)
}
if roundTrip := got.ToUint64(); roundTrip != binary.LittleEndian.Uint64(encoded) {
t.Fatalf("ToUint64 = %#x, want %#x", roundTrip, binary.LittleEndian.Uint64(encoded))
}
}
func TestParseFileReferenceZeroExtendsSixByteRecordNumber(t *testing.T) {
encoded := []byte{0x54, 0x76, 0x98, 0xBA, 0x00, 0x00, 0x34, 0x12}
got, err := ParseFileReference(encoded)
if err != nil {
t.Fatalf("ParseFileReference returned error: %v", err)
}
if got.RecordNumber != 0x00000000BA987654 {
t.Fatalf("RecordNumber = %#x, want %#x", got.RecordNumber, 0x00000000BA987654)
}
if got.SequenceNumber != 0x1234 {
t.Fatalf("SequenceNumber = %#x, want %#x", got.SequenceNumber, 0x1234)
}
}
func TestParseDataRunsSignExtendsOffset(t *testing.T) {
runs, err := ParseDataRuns([]byte{0x11, 0x02, 0xFE, 0x00})
if err != nil {
t.Fatalf("ParseDataRuns returned error: %v", err)
}
if len(runs) != 1 {
t.Fatalf("len(runs) = %d, want 1", len(runs))
}
if runs[0].LengthInClusters != 2 {
t.Fatalf("LengthInClusters = %d, want 2", runs[0].LengthInClusters)
}
if runs[0].OffsetCluster != -2 {
t.Fatalf("OffsetCluster = %d, want -2", runs[0].OffsetCluster)
}
}
func TestApplyFixUpRejectsInvalidSequenceRange(t *testing.T) {
data := make([]byte, 1024)
if _, err := applyFixUp(data, 1020, 4); err == nil {
t.Fatal("expected applyFixUp to reject out-of-range update sequence")
}
}
func TestParseRecordRejectsInvalidFixupWithoutPanic(t *testing.T) {
data := make([]byte, 1024)
copy(data[:4], fileSignature)
binary.LittleEndian.PutUint16(data[0x14:], 0x2A)
defer func() {
if r := recover(); r != nil {
t.Fatalf("ParseRecord panicked: %v", r)
}
}()
if _, err := ParseRecord(data); err == nil {
t.Fatal("expected ParseRecord to reject invalid fixup data")
}
}
+260 -223
View File
@@ -1,17 +1,12 @@
package mft
import (
"b612.me/wincmd/ntfs/binutil"
"b612.me/wincmd/ntfs/utf16"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"reflect"
"runtime"
"strings"
"time"
"unsafe"
)
type MFTFile struct {
@@ -22,126 +17,27 @@ type MFTFile struct {
Aszie uint64
IsDir bool
Node uint64
Parent uint64
}
type FileEntry struct {
Name string
Parent uint64
}
const (
defaultMFTRecordSize = int64(1024)
maxMFTBatchRecords = int64(1024)
)
func GetFileListsByMftFn(driver string, fn func(string, bool) bool) ([]MFTFile, error) {
var result []MFTFile
extendMftRecord := make(map[uint64][]Attribute)
fileMap := make(map[uint64]FileEntry)
f, size, err := GetMFTFile(driver)
reader, size, recordSize, err := openMFTFile(driver)
if err != nil {
return []MFTFile{}, err
}
recordSize := int64(1024)
alreadyGot := int64(0)
maxRecordSize := size / recordSize
if maxRecordSize > 1024 {
maxRecordSize = 1024
}
for {
for {
if (size - alreadyGot) < maxRecordSize*recordSize {
maxRecordSize--
} else {
break
}
}
if maxRecordSize < 10 {
maxRecordSize = 1
}
buf := make([]byte, maxRecordSize*recordSize)
got, err := io.ReadFull(f, buf)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return []MFTFile{}, err
}
alreadyGot += int64(got)
for j := int64(0); j < 1024*maxRecordSize; j += 1024 {
record, err := ParseRecord(buf[j : j+1024])
if err != nil {
continue
}
if record.BaseRecordReference.ToUint64() != 0 {
val := extendMftRecord[record.BaseRecordReference.ToUint64()]
for _, v := range record.Attributes {
if v.Type == AttributeTypeData && v.ActualSize != 0 {
val = append(val, v)
}
}
if len(val) != 0 {
extendMftRecord[record.BaseRecordReference.ToUint64()] = val
}
}
if record.Flags&RecordFlagInUse == 1 && record.Flags&RecordFlagIsIndex == 0 {
var file MFTFile
file.IsDir = record.Flags&RecordFlagIsDirectory != 0
file.Node = record.FileReference.ToUint64()
parent := uint64(0)
for _, v := range record.Attributes {
if v.Type == AttributeTypeData {
file.Size = v.ActualSize
file.Aszie = v.AllocatedSize
}
if v.Type == AttributeTypeStandardInformation {
if len(v.Data) >= 48 {
r := binutil.NewLittleEndianReader(v.Data)
file.ModTime = ConvertFileTime(r.Uint64(0x08))
}
}
if v.Type == AttributeTypeFileName {
name := utf16.DecodeString(v.Data[66:], binary.LittleEndian)
if len(file.Name) < len(name) && len(name) > 0 {
if len(file.Name) > 0 && !strings.Contains(file.Name, "~") {
continue
}
file.Name = name
}
if file.Name != "" {
parent = binutil.NewLittleEndianReader(v.Data[:8]).Uint64(0)
}
}
}
defer reader.Close()
if file.Name != "" {
canAdd := fn(file.Name, file.IsDir)
if canAdd {
result = append(result, file)
}
if canAdd || file.IsDir {
fileMap[uint64(file.Node)] = FileEntry{
Name: file.Name,
Parent: uint64(parent),
}
}
}
}
}
}
(*reflect.SliceHeader)(unsafe.Pointer(&result)).Cap = len(result)
for k, v := range result {
if attrs, ok := extendMftRecord[v.Node]; ok {
if v.Aszie == 0 {
for _, v := range attrs {
if v.Type == AttributeTypeData && v.ActualSize != 0 {
result[k].Size = v.ActualSize
result[k].Aszie = v.AllocatedSize
}
}
}
delete(extendMftRecord, v.Node)
}
result[k].Path = GetFullUsnPath(driver, fileMap, uint64(v.Node))
}
fileMap = nil
runtime.GC()
return result, nil
return collectMFTFiles(driver, reader, size, recordSize, fn)
}
func GetFileListsByMft(driver string) ([]MFTFile, error) {
@@ -149,129 +45,51 @@ func GetFileListsByMft(driver string) ([]MFTFile, error) {
}
func GetFileListsFromMftFileFn(filepath string, fn func(string, bool) bool) ([]MFTFile, error) {
var result []MFTFile
extendMftRecord := make(map[uint64][]Attribute)
fileMap := make(map[uint64]FileEntry)
f, err := os.Open(filepath)
if err != nil {
return []MFTFile{}, err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return []MFTFile{}, err
}
size := stat.Size()
recordSize := int64(1024)
alreadyGot := int64(0)
maxRecordSize := size / recordSize
if maxRecordSize > 1024 {
maxRecordSize = 1024
}
for {
for {
if (size - alreadyGot) < maxRecordSize*recordSize {
maxRecordSize--
} else {
break
}
}
if maxRecordSize < 10 {
maxRecordSize = 1
}
buf := make([]byte, maxRecordSize*recordSize)
got, err := io.ReadFull(f, buf)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return []MFTFile{}, err
}
alreadyGot += int64(got)
for j := int64(0); j < 1024*maxRecordSize; j += 1024 {
record, err := ParseRecord(buf[j : j+1024])
if err != nil {
continue
}
if record.BaseRecordReference.ToUint64() != 0 {
val := extendMftRecord[record.BaseRecordReference.ToUint64()]
for _, v := range record.Attributes {
if v.Type == AttributeTypeData && v.ActualSize != 0 {
val = append(val, v)
}
}
if len(val) != 0 {
extendMftRecord[record.BaseRecordReference.ToUint64()] = val
}
}
if record.Flags&RecordFlagInUse == 1 && record.Flags&RecordFlagIsIndex == 0 {
var file MFTFile
file.IsDir = record.Flags&RecordFlagIsDirectory != 0
file.Node = record.FileReference.ToUint64()
parent := uint64(0)
for _, v := range record.Attributes {
if v.Type == AttributeTypeData {
file.Size = v.ActualSize
file.Aszie = v.AllocatedSize
}
if v.Type == AttributeTypeStandardInformation {
if len(v.Data) >= 48 {
r := binutil.NewLittleEndianReader(v.Data)
file.ModTime = ConvertFileTime(r.Uint64(0x08))
}
}
if v.Type == AttributeTypeFileName {
name := utf16.DecodeString(v.Data[66:], binary.LittleEndian)
if len(file.Name) < len(name) && len(name) > 0 {
if len(file.Name) > 0 && !strings.Contains(file.Name, "~") {
continue
}
file.Name = name
}
if file.Name != "" {
parent = binutil.NewLittleEndianReader(v.Data[:8]).Uint64(0)
}
}
}
if file.Name != "" {
canAdd := fn(file.Name, file.IsDir)
if canAdd {
result = append(result, file)
}
if canAdd || file.IsDir {
fileMap[uint64(file.Node)] = FileEntry{
Name: file.Name,
Parent: uint64(parent),
}
}
}
}
}
}
(*reflect.SliceHeader)(unsafe.Pointer(&result)).Cap = len(result)
for k, v := range result {
if attrs, ok := extendMftRecord[v.Node]; ok {
if v.Aszie == 0 {
for _, v := range attrs {
if v.Type == AttributeTypeData && v.ActualSize != 0 {
result[k].Size = v.ActualSize
result[k].Aszie = v.AllocatedSize
}
}
}
delete(extendMftRecord, v.Node)
}
result[k].Path = GetFullUsnPath(" ", fileMap, uint64(v.Node))
}
fileMap = nil
runtime.GC()
return result, nil
return collectMFTFiles(" ", f, stat.Size(), defaultMFTRecordSize, fn)
}
func GetFileListsFromMftFile(filepath string) ([]MFTFile, error) {
return GetFileListsFromMftFileFn(filepath, func(string, bool) bool { return true })
}
// WalkRecordsByMFT walks parsed MFT records from a live NTFS volume.
func WalkRecordsByMFT(driver string, fn func(Record) error) error {
reader, size, recordSize, err := openMFTFile(driver)
if err != nil {
return err
}
defer reader.Close()
return walkRecords(reader, size, recordSize, ParseRecord, fn)
}
// WalkRecordsFromMFTFile walks parsed MFT records from a dumped $MFT file.
func WalkRecordsFromMFTFile(filepath string, fn func(Record) error) error {
f, err := os.Open(filepath)
if err != nil {
return err
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return err
}
return walkRecords(f, stat.Size(), defaultMFTRecordSize, ParseRecord, fn)
}
func GetFullUsnPath(diskName string, fileMap map[uint64]FileEntry, id uint64) (name string) {
for id != 0 {
fe := fileMap[id]
@@ -289,3 +107,222 @@ func GetFullUsnPath(diskName string, fileMap map[uint64]FileEntry, id uint64) (n
name = diskName[:len(diskName)-1] + name
return
}
type extendedData struct {
Size uint64
AllocatedSize uint64
}
func collectMFTFiles(diskName string, reader io.Reader, size int64, recordSize int64, fn func(string, bool) bool) ([]MFTFile, error) {
if fn == nil {
fn = func(string, bool) bool { return true }
}
extendMFTRecord := make(map[uint64]extendedData)
fileMap := make(map[uint64]FileEntry)
result := make([]MFTFile, 0)
err := walkRecords(reader, size, recordSize, ParseRecord, func(record Record) error {
appendExtendedData(extendMFTRecord, record)
file, ok := FileFromRecord(record)
if !ok {
return nil
}
canAdd := fn(file.Name, file.IsDir)
if canAdd {
result = append(result, file)
}
if canAdd || file.IsDir {
fileMap[file.Node] = FileEntry{
Name: file.Name,
Parent: file.Parent,
}
}
return nil
})
if err != nil {
return nil, err
}
for i := range result {
if attrs, ok := extendMFTRecord[result[i].Node]; ok {
if result[i].Aszie == 0 {
applyExtendedData(&result[i], attrs)
}
delete(extendMFTRecord, result[i].Node)
}
result[i].Path = GetFullUsnPath(diskName, fileMap, result[i].Node)
}
return result, nil
}
func walkRecords(reader io.Reader, size int64, recordSize int64, parser func([]byte) (Record, error), visit func(Record) error) error {
if recordSize <= 0 {
return fmt.Errorf("invalid MFT record size %d", recordSize)
}
if recordSize > maxInt {
return fmt.Errorf("MFT record size %d overflows maximum int value %d", recordSize, maxInt)
}
if parser == nil {
return fmt.Errorf("nil MFT record parser")
}
if visit == nil {
return fmt.Errorf("nil MFT record visitor")
}
chunkSize := recordSize * maxMFTBatchRecords
if chunkSize <= 0 {
chunkSize = recordSize
}
if size > 0 && chunkSize > size {
chunkSize = size
}
if chunkSize <= 0 {
chunkSize = recordSize
}
intRecordSize := int(recordSize)
buf := make([]byte, int(chunkSize))
for {
got, err := io.ReadFull(reader, buf)
if err != nil {
if errors.Is(err, io.EOF) && got == 0 {
return nil
}
if !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
return err
}
}
if got == 0 {
return nil
}
usable := got - got%intRecordSize
for offset := 0; offset < usable; offset += intRecordSize {
record, err := parser(buf[offset : offset+intRecordSize])
if err != nil {
continue
}
if err := visit(record); err != nil {
return err
}
}
if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) {
return nil
}
}
}
func appendExtendedData(extended map[uint64]extendedData, record Record) {
baseRecord := record.BaseRecordReference.ToUint64()
if baseRecord == 0 {
return
}
for _, attr := range record.Attributes {
if attr.Type == AttributeTypeData && attr.ActualSize != 0 {
extended[baseRecord] = extendedData{
Size: attr.ActualSize,
AllocatedSize: attr.AllocatedSize,
}
}
}
}
// FileFromRecord extracts a high-level file entry from a parsed MFT record.
func FileFromRecord(record Record) (MFTFile, bool) {
if record.Flags&RecordFlagInUse == 0 || record.Flags&RecordFlagIsIndex != 0 {
return MFTFile{}, false
}
file := MFTFile{
IsDir: record.Flags&RecordFlagIsDirectory != 0,
Node: record.FileReference.ToUint64(),
}
bestNamespace := FileNameNamespace(0)
for _, attr := range record.Attributes {
switch attr.Type {
case AttributeTypeData:
file.Size = attr.ActualSize
file.Aszie = attr.AllocatedSize
case AttributeTypeStandardInformation:
info, err := ParseStandardInformation(attr.Data)
if err == nil {
file.ModTime = info.FileLastModified
}
case AttributeTypeFileName:
name, nameParent, namespace, ok := bestFileName(file.Name, bestNamespace, attr.Data)
if ok {
file.Name = name
file.Parent = nameParent
bestNamespace = namespace
}
}
}
if file.Name == "" {
return MFTFile{}, false
}
return file, true
}
func bestFileName(current string, currentNamespace FileNameNamespace, data []byte) (string, uint64, FileNameNamespace, bool) {
fileName, err := ParseFileName(data)
if err != nil || fileName.Name == "" {
return current, 0, currentNamespace, false
}
if !shouldPreferFileNameWithNamespace(current, currentNamespace, fileName.Name, fileName.Namespace) {
return current, 0, currentNamespace, false
}
return fileName.Name, fileName.ParentFileReference.ToUint64(), fileName.Namespace, true
}
func shouldPreferFileName(current string, candidate string) bool {
return shouldPreferFileNameWithNamespace(current, 0, candidate, 0)
}
func shouldPreferFileNameWithNamespace(current string, currentNamespace FileNameNamespace, candidate string, candidateNamespace FileNameNamespace) bool {
if candidate == "" {
return false
}
if current == "" {
return true
}
currentRank := fileNameNamespaceRank(currentNamespace)
candidateRank := fileNameNamespaceRank(candidateNamespace)
if currentRank != candidateRank {
return candidateRank > currentRank
}
currentShort := strings.Contains(current, "~")
candidateShort := strings.Contains(candidate, "~")
if currentShort != candidateShort {
return currentShort && !candidateShort
}
return len(candidate) > len(current)
}
func fileNameNamespaceRank(namespace FileNameNamespace) int {
switch namespace {
case FileNameNamespaceWin32, FileNameNamespaceWin32Dos:
return 3
case FileNameNamespacePosix:
return 2
case FileNameNamespaceDos:
return 1
default:
return 0
}
}
func applyExtendedData(file *MFTFile, data extendedData) {
file.Size = data.Size
file.Aszie = data.AllocatedSize
}
+170
View File
@@ -0,0 +1,170 @@
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
}
+74 -48
View File
@@ -15,13 +15,17 @@ const supportedOemId = "NTFS "
const isWin = runtime.GOOS == "windows"
func GetMFTFileBytes(volume string) ([]byte, error) {
reader, length, err := GetMFTFile(volume)
reader, length, err := GetMFTFileReader(volume)
if err != nil {
return nil, err
}
buf := make([]byte, length)
bfio := bytes.NewBuffer(buf)
defer reader.Close()
bfio := bytes.NewBuffer(make([]byte, 0, length))
written, err := copyBytes(bfio, reader, length)
if err != nil {
return nil, err
}
if written != length {
return nil, fmt.Errorf("Write Not Ok,Should %d got %d", length, written)
}
@@ -29,16 +33,21 @@ func GetMFTFileBytes(volume string) ([]byte, error) {
}
func DumpMFTFile(volume, filepath string, fn func(int64, int64, float64)) error {
reader, length, err := GetMFTFile(volume)
reader, length, err := GetMFTFileReader(volume)
if err != nil {
return err
}
defer reader.Close()
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
written, err := copyFiles(out, reader, length, fn)
if err != nil {
return err
}
if written != length {
return fmt.Errorf("Write Not Ok,Should %d got %d", length, written)
}
@@ -46,69 +55,98 @@ func DumpMFTFile(volume, filepath string, fn func(int64, int64, float64)) error
}
func GetMFTFile(volume string) (io.Reader, int64, error) {
reader, length, err := GetMFTFileReader(volume)
if err != nil {
return nil, 0, err
}
return reader, length, nil
}
func GetMFTFileReader(volume string) (io.ReadCloser, int64, error) {
reader, length, _, err := openMFTFile(volume)
if err != nil {
return nil, 0, err
}
return reader, length, nil
}
func openMFTFile(volume string) (io.ReadCloser, int64, int64, error) {
if isWin {
volume = `\\.\` + volume[:len(volume)-1]
}
in, err := os.Open(volume)
if err != nil {
return nil, 0, err
return nil, 0, 0, err
}
success := false
defer func() {
if !success {
in.Close()
}
}()
bootSectorData := make([]byte, 512)
_, err = io.ReadFull(in, bootSectorData)
if err != nil {
return nil, 0, fmt.Errorf("Unable to read boot sector: %v\n", err)
return nil, 0, 0, fmt.Errorf("Unable to read boot sector: %v", err)
}
bootSector, err := bootsect.Parse(bootSectorData)
if err != nil {
return nil, 0, fmt.Errorf("Unable to parse boot sector data: %v\n", err)
return nil, 0, 0, fmt.Errorf("Unable to parse boot sector data: %v", err)
}
if bootSector.OemId != supportedOemId {
return nil, 0, fmt.Errorf("Unknown OemId (file system type) %q (expected %q)\n", bootSector.OemId, supportedOemId)
return nil, 0, 0, fmt.Errorf("Unknown OemId (file system type) %q (expected %q)", bootSector.OemId, supportedOemId)
}
bytesPerCluster := bootSector.BytesPerSector * bootSector.SectorsPerCluster
if bytesPerCluster <= 0 {
return nil, 0, 0, fmt.Errorf("Invalid bytes per cluster %d", bytesPerCluster)
}
mftPosInBytes := int64(bootSector.MftClusterNumber) * int64(bytesPerCluster)
_, err = in.Seek(mftPosInBytes, 0)
if err != nil {
return nil, 0, fmt.Errorf("Unable to seek to MFT position: %v\n", err)
return nil, 0, 0, fmt.Errorf("Unable to seek to MFT position: %v", err)
}
mftSizeInBytes := bootSector.FileRecordSegmentSizeInBytes
if mftSizeInBytes <= 0 {
return nil, 0, 0, fmt.Errorf("Invalid MFT record size %d", mftSizeInBytes)
}
mftData := make([]byte, mftSizeInBytes)
_, err = io.ReadFull(in, mftData)
if err != nil {
return nil, 0, fmt.Errorf("Unable to read $MFT record: %v\n", err)
return nil, 0, 0, fmt.Errorf("Unable to read $MFT record: %v", err)
}
record, err := ParseRecord(mftData)
if err != nil {
return nil, 0, fmt.Errorf("Unable to parse $MFT record: %v\n", err)
return nil, 0, 0, fmt.Errorf("Unable to parse $MFT record: %v", err)
}
dataAttributes := record.FindAttributes(AttributeTypeData)
if len(dataAttributes) == 0 {
return nil, 0, fmt.Errorf("No $DATA attribute found in $MFT record\n")
return nil, 0, 0, fmt.Errorf("No $DATA attribute found in $MFT record")
}
if len(dataAttributes) > 1 {
return nil, 0, fmt.Errorf("More than 1 $DATA attribute found in $MFT record\n")
return nil, 0, 0, fmt.Errorf("More than 1 $DATA attribute found in $MFT record")
}
dataAttribute := dataAttributes[0]
if dataAttribute.Resident {
return nil, 0, fmt.Errorf("Don't know how to handle resident $DATA attribute in $MFT record\n")
return nil, 0, 0, fmt.Errorf("Don't know how to handle resident $DATA attribute in $MFT record")
}
dataRuns, err := ParseDataRuns(dataAttribute.Data)
if err != nil {
return nil, 0, fmt.Errorf("Unable to parse dataruns in $MFT $DATA record: %v\n", err)
return nil, 0, 0, fmt.Errorf("Unable to parse dataruns in $MFT $DATA record: %v", err)
}
if len(dataRuns) == 0 {
return nil, 0, fmt.Errorf("No dataruns found in $MFT $DATA record\n")
return nil, 0, 0, fmt.Errorf("No dataruns found in $MFT $DATA record")
}
fragments := DataRunsToFragments(dataRuns, bytesPerCluster)
@@ -117,47 +155,24 @@ func GetMFTFile(volume string) (io.Reader, int64, error) {
totalLength += int64(frag.Length)
}
return fragment.NewReader(in, fragments), totalLength, nil
success = true
return fragment.NewReader(in, fragments), totalLength, int64(mftSizeInBytes), nil
}
func copyBytes(dst io.Writer, src io.Reader, totalLength int64) (written int64, err error) {
buf := make([]byte, 1024*1024)
// Below copied from io.copyBuffer (https://golang.org/src/io/io.go?s=12796:12856#L380)
for {
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
}
return written, err
return copyWithProgress(dst, src, totalLength, nil)
}
func copyFiles(dst io.Writer, src io.Reader, totalLength int64, fn func(int64, int64, float64)) (written int64, err error) {
return copyWithProgress(dst, src, totalLength, fn)
}
func copyWithProgress(dst io.Writer, src io.Reader, totalLength int64, fn func(int64, int64, float64)) (written int64, err error) {
buf := make([]byte, 1024*1024)
onePercent := float64(written) / float64(totalLength) * float64(100.0)
// Below copied from io.copyBuffer (https://golang.org/src/io/io.go?s=12796:12856#L380)
for {
fn(written, totalLength, onePercent)
reportCopyProgress(fn, written, totalLength)
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
@@ -180,6 +195,17 @@ func copyFiles(dst io.Writer, src io.Reader, totalLength int64, fn func(int64, i
break
}
}
fn(written, totalLength, onePercent)
reportCopyProgress(fn, written, totalLength)
return written, err
}
func reportCopyProgress(fn func(int64, int64, float64), written int64, totalLength int64) {
if fn == nil {
return
}
if totalLength <= 0 {
fn(written, totalLength, 100)
return
}
fn(written, totalLength, float64(written)/float64(totalLength)*100)
}
+3
View File
@@ -50,6 +50,9 @@ func newFileStatFromInformation(d *syscall.ByHandleFileInformation, name string,
LastWriteTime: d.LastWriteTime,
FileSizeHigh: d.FileSizeHigh,
FileSizeLow: d.FileSizeLow,
vol: d.VolumeSerialNumber,
idxhi: d.FileIndexHigh,
idxlo: d.FileIndexLow,
}
}
+393 -6
View File
@@ -1,13 +1,400 @@
package usn
import (
"fmt"
"encoding/binary"
"errors"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"unicode/utf16"
"b612.me/win32api"
)
func Test_USN(t *testing.T) {
fmt.Println("start")
data, err := ListUsnFile("C:\\")
fmt.Println(err)
fmt.Println(len(data))
func TestGetPointerUsesSliceLength(t *testing.T) {
buf := make([]uint16, 3, 16)
_, size, err := getPointer(buf)
if err != nil {
t.Fatalf("getPointer failed: %v", err)
}
if want := uintptr(len(buf)) * uintptr(2); size != want {
t.Fatalf("slice size = %d, want %d", size, want)
}
}
func TestParseUSNOutput(t *testing.T) {
buf := buildTestUSNBuffer(1234, "hello.txt", false, 0x20)
var got usnRecordData
next, err := parseUSNOutput(buf, uint32(len(buf)), func(record usnRecordData) error {
got = record
return nil
})
if err != nil {
t.Fatalf("parseUSNOutput failed: %v", err)
}
if next != 1234 {
t.Fatalf("next = %d, want 1234", next)
}
if got.FileName != "hello.txt" {
t.Fatalf("FileName = %q, want %q", got.FileName, "hello.txt")
}
if got.FileReferenceNumber != 100 {
t.Fatalf("FileReferenceNumber = %d, want 100", got.FileReferenceNumber)
}
if got.ParentFileReferenceNumber != 55 {
t.Fatalf("ParentFileReferenceNumber = %d, want 55", got.ParentFileReferenceNumber)
}
if got.Reason != 0x20 {
t.Fatalf("Reason = %#x, want %#x", got.Reason, 0x20)
}
}
func TestParseUSNOutputRejectsShortRecord(t *testing.T) {
buf := buildTestUSNBuffer(1, "bad", false, 0)
binary.LittleEndian.PutUint32(buf[usnBufferHeaderSize:], uint32(usnRecordMinSize-2))
if _, err := parseUSNOutput(buf, uint32(len(buf)), func(usnRecordData) error { return nil }); err == nil {
t.Fatal("expected parseUSNOutput to reject short record")
}
}
func TestShouldPreferUSNFileName(t *testing.T) {
tests := []struct {
current string
candidate string
want bool
}{
{current: "", candidate: "Program Files", 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},
{current: "Program Files", candidate: "program files", want: false},
}
for _, tt := range tests {
if got := shouldPreferUSNFileName(tt.current, tt.candidate); got != tt.want {
t.Fatalf("shouldPreferUSNFileName(%q, %q) = %v, want %v", tt.current, tt.candidate, got, tt.want)
}
}
}
func TestMergeUSNFileEntryPrefersLongName(t *testing.T) {
current := FileEntry{Name: "PROGRA~1", Parent: 7}
candidate := FileEntry{Name: "Program Files", Parent: 9}
merged := mergeUSNFileEntry(current, candidate)
if merged.Name != "Program Files" {
t.Fatalf("Name = %q, want %q", merged.Name, "Program Files")
}
if merged.Parent != 9 {
t.Fatalf("Parent = %d, want 9", merged.Parent)
}
}
func TestMergeUSNFileEntryTracksRename(t *testing.T) {
current := FileEntry{Name: "alpha.txt", Parent: 7}
candidate := FileEntry{Name: "omega.txt", Parent: 7}
merged := mergeUSNFileEntry(current, candidate)
if merged.Name != "omega.txt" {
t.Fatalf("Name = %q, want %q", merged.Name, "omega.txt")
}
}
func TestFilterUSNFileMapUsesFinalName(t *testing.T) {
fileMap := map[win32api.DWORDLONG]FileEntry{
1: {Name: "Windows", Parent: 1, Type: 1},
2: {Name: "Program Files", Parent: 1, Type: 0},
3: {Name: "Temp", Parent: 1, Type: 0},
}
filtered := filterUSNFileMap(fileMap, func(name string, _ bool) bool {
return strings.Contains(name, "Program")
})
if _, ok := filtered[1]; !ok {
t.Fatal("expected directory entry to be retained")
}
if _, ok := filtered[2]; !ok {
t.Fatal("expected matching file entry to be retained")
}
if _, ok := filtered[3]; ok {
t.Fatal("did not expect non-matching file entry to be retained")
}
}
func TestNeedPathCanonicalNameOverlay(t *testing.T) {
if needPathCanonicalNameOverlay(map[win32api.DWORDLONG]FileEntry{
1: {Name: "Program Files", Parent: 1},
}) {
t.Fatal("did not expect overlay for long names only")
}
if !needPathCanonicalNameOverlay(map[win32api.DWORDLONG]FileEntry{
1: {Name: "PROGRA~1", Parent: 1},
}) {
t.Fatal("expected overlay when short name exists")
}
}
func TestWindowsBaseName(t *testing.T) {
if got := windowsBaseName(`C:\Program Files\`); got != "Program Files" {
t.Fatalf("windowsBaseName returned %q", got)
}
if got := windowsBaseName(`C:\Windows\System32`); got != "System32" {
t.Fatalf("windowsBaseName returned %q", got)
}
if got := windowsBaseName(`single`); got != "single" {
t.Fatalf("windowsBaseName returned %q", got)
}
}
func TestApplyPathCanonicalNamesUsesNormalizedPath(t *testing.T) {
origNormalize := normalizePathForUSN
defer func() {
normalizePathForUSN = origNormalize
}()
normalizePathForUSN = func(path string) string {
if strings.Contains(path, "PROGRA~1") {
return strings.Replace(path, "PROGRA~1", "Program Files", 1)
}
return path
}
fileMap := map[win32api.DWORDLONG]FileEntry{
1: {Name: "", Parent: 1, Type: 1},
2: {Name: "PROGRA~1", Parent: 1, Type: 0},
}
applyPathCanonicalNames("C:\\", fileMap)
entry := fileMap[2]
if entry.Name != "Program Files" {
t.Fatalf("Name = %q, want %q", entry.Name, "Program Files")
}
if entry.Parent != 1 {
t.Fatalf("Parent = %d, want 1", entry.Parent)
}
}
func TestApplyPathCanonicalNamesSkipsWhenNotNeeded(t *testing.T) {
origNormalize := normalizePathForUSN
defer func() {
normalizePathForUSN = origNormalize
}()
called := false
normalizePathForUSN = func(path string) string {
called = true
return path
}
fileMap := map[win32api.DWORDLONG]FileEntry{
2: {Name: "Program Files", Parent: 1, Type: 0},
}
applyPathCanonicalNames("C:\\", fileMap)
if called {
t.Fatal("did not expect normalization when no short names exist")
}
}
func TestFileStatFromIDWithfd(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "usn-by-id.txt")
content := []byte("usn by id test")
if err := os.WriteFile(path, content, 0600); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
volume := filepath.VolumeName(path) + `\`
info, err := GetDiskInfo(volume)
if err != nil {
t.Fatalf("GetDiskInfo failed: %v", err)
}
if !strings.EqualFold(info.Format, "NTFS") {
t.Skipf("volume %s is %s, not NTFS", volume, info.Format)
}
file, err := os.Open(path)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer file.Close()
var handleInfo syscall.ByHandleFileInformation
if err := syscall.GetFileInformationByHandle(syscall.Handle(file.Fd()), &handleInfo); err != nil {
t.Fatalf("GetFileInformationByHandle failed: %v", err)
}
volumeHandle, err := CreateFile(`\\.\`+volume[:len(volume)-1], syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
if err != nil {
if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
t.Skipf("opening volume handle requires extra privilege: %v", err)
}
t.Fatalf("CreateFile(volume) failed: %v", err)
}
defer syscall.Close(volumeHandle)
fileID := win32api.DWORDLONG(uint64(handleInfo.FileIndexHigh)<<32 | uint64(handleInfo.FileIndexLow))
stat, err := fileStatFromIDWithfd(volumeHandle, fileID, filepath.Base(path), path, 0)
if err != nil {
t.Fatalf("fileStatFromIDWithfd failed: %v", err)
}
if stat.Name() != filepath.Base(path) {
t.Fatalf("Name = %q, want %q", stat.Name(), filepath.Base(path))
}
if stat.Size() != int64(len(content)) {
t.Fatalf("Size = %d, want %d", stat.Size(), len(content))
}
if stat.vol != handleInfo.VolumeSerialNumber || stat.idxhi != handleInfo.FileIndexHigh || stat.idxlo != handleInfo.FileIndexLow {
t.Fatal("file identifiers do not match source handle info")
}
}
func TestCollectUSNFileStatsSkipsFailedFetch(t *testing.T) {
data := map[win32api.DWORDLONG]FileEntry{
1: {Name: "keep-a.txt", Parent: 1, Type: 0},
2: {Name: "drop-b.txt", Parent: 1, Type: 0},
3: {Name: "keep-c", Parent: 1, Type: 1},
}
got := collectUSNFileStats(data, nil, func(id win32api.DWORDLONG, entry FileEntry) (FileStat, error) {
if id == 2 {
return FileStat{}, errors.New("fetch failed")
}
stat := FileStat{name: entry.Name}
if entry.Type == 1 {
stat.FileAttributes = win32api.FILE_ATTRIBUTE_DIRECTORY
}
return stat, nil
})
if len(got) != 2 {
t.Fatalf("len(got) = %d, want 2", len(got))
}
names := map[string]bool{}
for _, stat := range got {
names[stat.Name()] = true
if stat.Name() == "" {
t.Fatal("expected failed fetch entries to be skipped instead of zero-value placeholders")
}
}
if !names["keep-a.txt"] || !names["keep-c"] {
t.Fatalf("unexpected names: %+v", names)
}
if names["drop-b.txt"] {
t.Fatal("did not expect failed fetch entry in results")
}
}
func TestCollectUSNFileStatsAppliesFilter(t *testing.T) {
data := map[win32api.DWORDLONG]FileEntry{
1: {Name: "keep-file.txt", Parent: 1, Type: 0},
2: {Name: "skip-file.txt", Parent: 1, Type: 0},
3: {Name: "keep-dir", Parent: 1, Type: 1},
}
got := collectUSNFileStats(data, func(name string, _ bool) bool {
return strings.HasPrefix(name, "keep-")
}, func(_ win32api.DWORDLONG, entry FileEntry) (FileStat, error) {
stat := FileStat{name: entry.Name}
if entry.Type == 1 {
stat.FileAttributes = win32api.FILE_ATTRIBUTE_DIRECTORY
}
return stat, nil
})
if len(got) != 2 {
t.Fatalf("len(got) = %d, want 2", len(got))
}
for _, stat := range got {
if !strings.HasPrefix(stat.Name(), "keep-") {
t.Fatalf("unexpected stat name %q", stat.Name())
}
}
}
func TestCollectUSNFileStatsNilFilterIncludesAll(t *testing.T) {
data := map[win32api.DWORDLONG]FileEntry{
1: {Name: "a.txt", Parent: 1, Type: 0},
2: {Name: "b.txt", Parent: 1, Type: 0},
3: {Name: "c", Parent: 1, Type: 1},
}
got := collectUSNFileStats(data, nil, func(_ win32api.DWORDLONG, entry FileEntry) (FileStat, error) {
return FileStat{name: entry.Name}, nil
})
if len(got) != len(data) {
t.Fatalf("len(got) = %d, want %d", len(got), len(data))
}
}
func TestCollectUSNFileStatsNilFetchReturnsEmpty(t *testing.T) {
data := map[win32api.DWORDLONG]FileEntry{
1: {Name: "a.txt", Parent: 1, Type: 0},
2: {Name: "b.txt", Parent: 1, Type: 0},
}
got := collectUSNFileStats(data, nil, nil)
if len(got) != 0 {
t.Fatalf("len(got) = %d, want 0", len(got))
}
}
func buildTestUSNBuffer(next uint64, name string, isDir bool, reason uint32) []byte {
encoded := utf16.Encode([]rune(name))
nameBytes := make([]byte, len(encoded)*2)
for i, v := range encoded {
binary.LittleEndian.PutUint16(nameBytes[i*2:], v)
}
recordLength := usnRecordMinSize + len(nameBytes)
buf := make([]byte, usnBufferHeaderSize+recordLength)
binary.LittleEndian.PutUint64(buf[:usnBufferHeaderSize], next)
record := buf[usnBufferHeaderSize:]
binary.LittleEndian.PutUint32(record, uint32(recordLength))
binary.LittleEndian.PutUint16(record[4:], 2)
binary.LittleEndian.PutUint16(record[6:], 0)
binary.LittleEndian.PutUint64(record[usnRecordOffsetFileReference:], 100)
binary.LittleEndian.PutUint64(record[usnRecordOffsetParentReference:], 55)
binary.LittleEndian.PutUint32(record[usnRecordOffsetReason:], reason)
attrs := uint32(0)
if isDir {
attrs = win32api.FILE_ATTRIBUTE_DIRECTORY
}
binary.LittleEndian.PutUint32(record[usnRecordOffsetFileAttributes:], attrs)
binary.LittleEndian.PutUint16(record[usnRecordOffsetFileNameLength:], uint16(len(nameBytes)))
binary.LittleEndian.PutUint16(record[usnRecordOffsetFileNameOffset:], uint16(usnRecordMinSize))
copy(record[usnRecordMinSize:], nameBytes)
return buf
}
func TestNormalizeDiskName(t *testing.T) {
tests := map[string]string{
"c:": "C:\\",
"c:\\temp": "C:\\",
"D:/data": "D:\\",
}
for input, want := range tests {
got, err := normalizeDiskName(input)
if err != nil {
t.Fatalf("normalizeDiskName(%q) returned error: %v", input, err)
}
if got != want {
t.Fatalf("normalizeDiskName(%q) = %q, want %q", input, got, want)
}
}
if _, err := normalizeDiskName(""); err == nil {
t.Fatal("expected empty disk name error")
}
if _, err := normalizeDiskName("not-a-drive"); err == nil {
t.Fatal("expected invalid disk name error")
}
}
func TestUSNReasonStringUnknownHighBitDoesNotPanic(t *testing.T) {
got := USNReasonString(0x80000000)
if got == "" {
t.Fatal("expected non-empty reason string")
}
}
+492 -178
View File
@@ -3,10 +3,12 @@ package usn
import (
"b612.me/stario"
"b612.me/win32api"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"syscall"
"unsafe"
)
@@ -18,6 +20,29 @@ type DiskInfo struct {
SerialNumber uint32
}
func normalizeDiskName(diskName string) (string, error) {
name := strings.TrimSpace(strings.ReplaceAll(diskName, "/", "\\"))
if name == "" {
return "", fmt.Errorf("empty disk name")
}
volume := filepath.VolumeName(name)
if len(volume) == 2 && volume[1] == ':' {
return strings.ToUpper(volume[:1]) + ":\\", nil
}
if len(name) >= 2 && name[1] == ':' {
return strings.ToUpper(name[:1]) + ":\\", nil
}
return "", fmt.Errorf("invalid disk name: %q", diskName)
}
func volumeDevicePath(diskName string) (string, error) {
normalized, err := normalizeDiskName(diskName)
if err != nil {
return "", err
}
return "\\\\.\\" + strings.TrimSuffix(normalized, "\\"), nil
}
func ListDrivers() ([]string, error) {
drivers := make([]string, 0, 26)
buf := make([]uint16, 255)
@@ -70,27 +95,42 @@ func GetDiskInfo(disk string) (DiskInfo, error) {
}
func DeviceIoControl(handle syscall.Handle, controlCode uint32, in interface{}, out interface{}, done *uint32) (err error) {
inPtr, inSize := getPointer(in)
outPtr, outSize := getPointer(out)
inPtr, inSize, err := getPointer(in)
if err != nil {
return err
}
outPtr, outSize, err := getPointer(out)
if err != nil {
return err
}
//_,err = syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), uintptr(controlCode), inPtr, uintptr(inSize), outPtr, uintptr(outSize), uintptr(unsafe.Pointer(done)), uintptr(0), 0)
_, err = win32api.DeviceIoControlPtr(win32api.HANDLE(handle), win32api.DWORD(controlCode), inPtr, win32api.DWORD(inSize), outPtr, win32api.DWORD(outSize), done, nil)
return
}
func getPointer(i interface{}) (pointer, size uintptr) {
func getPointer(i interface{}) (pointer uintptr, size uintptr, err error) {
if i == nil {
return 0, 0, nil
}
v := reflect.ValueOf(i)
switch k := v.Kind(); k {
case reflect.Ptr:
if v.IsNil() {
return 0, 0, nil
}
t := v.Elem().Type()
size = t.Size()
pointer = v.Pointer()
case reflect.Slice:
size = uintptr(v.Cap())
if v.Len() == 0 {
return 0, 0, nil
}
size = uintptr(v.Len()) * v.Type().Elem().Size()
pointer = v.Pointer()
default:
fmt.Println("error")
return 0, 0, fmt.Errorf("unsupported DeviceIoControl buffer type %T", i)
}
return
return pointer, size, nil
}
// Need a custom Open to work with backup_semantics
@@ -179,13 +219,209 @@ type FileMonitor struct {
Reason string
}
func ListUsnFile(driver string) (map[win32api.DWORDLONG]FileEntry, error) {
var normalizePathForUSN = normalizeExistingLongPath
const (
usnBufferHeaderSize = int(unsafe.Sizeof(win32api.USN(0)))
usnRecordMinSize = int(unsafe.Offsetof(win32api.USN_RECORD{}.FileName))
usnRecordOffsetFileReference = int(unsafe.Offsetof(win32api.USN_RECORD{}.FileReferenceNumber))
usnRecordOffsetParentReference = int(unsafe.Offsetof(win32api.USN_RECORD{}.ParentFileReferenceNumber))
usnRecordOffsetReason = int(unsafe.Offsetof(win32api.USN_RECORD{}.Reason))
usnRecordOffsetFileAttributes = int(unsafe.Offsetof(win32api.USN_RECORD{}.FileAttributes))
usnRecordOffsetFileNameLength = int(unsafe.Offsetof(win32api.USN_RECORD{}.FileNameLength))
usnRecordOffsetFileNameOffset = int(unsafe.Offsetof(win32api.USN_RECORD{}.FileNameOffset))
)
type usnRecordData struct {
FileReferenceNumber win32api.DWORDLONG
ParentFileReferenceNumber win32api.DWORDLONG
Reason win32api.DWORD
FileAttributes win32api.DWORD
FileName string
}
func parseUSNOutput(data []byte, done uint32, fn func(usnRecordData) error) (uint64, error) {
if fn == nil {
return 0, fmt.Errorf("nil USN record callback")
}
if done == 0 {
return 0, nil
}
if done < uint32(usnBufferHeaderSize) {
return 0, fmt.Errorf("USN output too short: %d", done)
}
if int(done) > len(data) {
return 0, fmt.Errorf("USN output length %d exceeds buffer %d", done, len(data))
}
next := binary.LittleEndian.Uint64(data[:usnBufferHeaderSize])
for offset := usnBufferHeaderSize; offset < int(done); {
remaining := int(done) - offset
if remaining < usnRecordMinSize {
return next, fmt.Errorf("USN record header truncated: %d bytes remain", remaining)
}
recordLength := int(binary.LittleEndian.Uint32(data[offset:]))
if recordLength < usnRecordMinSize {
return next, fmt.Errorf("invalid USN record length %d", recordLength)
}
if recordLength > remaining {
return next, fmt.Errorf("USN record length %d exceeds remaining %d", recordLength, remaining)
}
record := data[offset : offset+recordLength]
nameLength := int(binary.LittleEndian.Uint16(record[usnRecordOffsetFileNameLength:]))
nameOffset := int(binary.LittleEndian.Uint16(record[usnRecordOffsetFileNameOffset:]))
if nameLength < 0 || nameLength%2 != 0 {
return next, fmt.Errorf("invalid USN file name length %d", nameLength)
}
if nameOffset < usnRecordMinSize || nameOffset > recordLength {
return next, fmt.Errorf("invalid USN file name offset %d", nameOffset)
}
if nameOffset+nameLength > recordLength {
return next, fmt.Errorf("USN file name exceeds record boundary: offset=%d length=%d record=%d", nameOffset, nameLength, recordLength)
}
name, err := decodeUTF16Bytes(record[nameOffset : nameOffset+nameLength])
if err != nil {
return next, err
}
entry := usnRecordData{
FileReferenceNumber: win32api.DWORDLONG(binary.LittleEndian.Uint64(record[usnRecordOffsetFileReference:])),
ParentFileReferenceNumber: win32api.DWORDLONG(binary.LittleEndian.Uint64(record[usnRecordOffsetParentReference:])),
Reason: win32api.DWORD(binary.LittleEndian.Uint32(record[usnRecordOffsetReason:])),
FileAttributes: win32api.DWORD(binary.LittleEndian.Uint32(record[usnRecordOffsetFileAttributes:])),
FileName: name,
}
if err := fn(entry); err != nil {
return next, err
}
offset += recordLength
}
return next, nil
}
func decodeUTF16Bytes(data []byte) (string, error) {
if len(data)%2 != 0 {
return "", fmt.Errorf("UTF-16 byte length must be even, got %d", len(data))
}
chars := make([]uint16, len(data)/2)
for i := range chars {
chars[i] = binary.LittleEndian.Uint16(data[i*2:])
}
return syscall.UTF16ToString(chars), nil
}
func fileEntryFromUSNRecord(record usnRecordData) FileEntry {
typed := uint8(0)
if record.FileAttributes&win32api.FILE_ATTRIBUTE_DIRECTORY != 0 {
typed = 1
}
return FileEntry{
Name: record.FileName,
Parent: record.ParentFileReferenceNumber,
Type: typed,
}
}
func shouldPreferUSNFileName(current string, candidate string) bool {
if candidate == "" {
return false
}
if current == "" {
return true
}
if strings.EqualFold(current, candidate) {
return false
}
currentShort := strings.Contains(current, "~")
candidateShort := strings.Contains(candidate, "~")
if currentShort != candidateShort {
return currentShort && !candidateShort
}
return len(candidate) > len(current)
}
func mergeUSNFileEntry(current FileEntry, candidate FileEntry) FileEntry {
if current.Name == "" && current.Parent == 0 && current.Type == 0 {
return candidate
}
merged := current
if shouldPreferUSNFileName(merged.Name, candidate.Name) {
merged.Name = candidate.Name
}
if candidate.Name != "" && !strings.EqualFold(merged.Name, candidate.Name) && !shouldPreferUSNFileName(candidate.Name, merged.Name) {
merged.Name = candidate.Name
}
if merged.Name == "" {
merged.Name = candidate.Name
}
if candidate.Parent != 0 {
merged.Parent = candidate.Parent
}
if candidate.Type == 1 {
merged.Type = 1
}
return merged
}
func needPathCanonicalNameOverlay(fileMap map[win32api.DWORDLONG]FileEntry) bool {
for _, entry := range fileMap {
if strings.Contains(entry.Name, "~") {
return true
}
}
return false
}
func windowsBaseName(path string) string {
trimmed := strings.TrimRight(path, `\/`)
if trimmed == "" {
return ""
}
last := strings.LastIndexAny(trimmed, `\/`)
if last < 0 {
return trimmed
}
return trimmed[last+1:]
}
func applyPathCanonicalNames(driver string, fileMap map[win32api.DWORDLONG]FileEntry) {
if len(fileMap) == 0 || !needPathCanonicalNameOverlay(fileMap) {
return
}
for id, entry := range fileMap {
if !strings.Contains(entry.Name, "~") {
continue
}
path := buildUSNPath(driver, fileMap, id)
normalized := normalizePathForUSN(path)
base := windowsBaseName(normalized)
if base == "" {
continue
}
entry.Name = base
fileMap[id] = entry
}
}
func buildUSNFileMap(driver string) (map[win32api.DWORDLONG]FileEntry, error) {
fileMap := make(map[win32api.DWORDLONG]FileEntry)
pDriver := "\\\\.\\" + driver[:len(driver)-1]
pDriver, err := volumeDevicePath(driver)
if err != nil {
return fileMap, err
}
fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
if err != nil {
return fileMap, err
}
defer syscall.Close(fd)
ujd, _, err := queryUsnJournal(fd)
if err != nil {
return fileMap, err
@@ -197,77 +433,51 @@ func ListUsnFile(driver string) (map[win32api.DWORDLONG]FileEntry, error) {
return fileMap, err
}
if done == 0 {
applyPathCanonicalNames(driver, fileMap)
return fileMap, nil
}
var usn win32api.USN = *(*win32api.USN)(unsafe.Pointer(&data[0]))
// fmt.Println("usn", usn)
var ur *win32api.USN_RECORD
for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
ur = (*win32api.USN_RECORD)(unsafe.Pointer(&data[i]))
nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
fnUtf := (*[10000]uint16)(fnp)[:nameLength]
fn := syscall.UTF16ToString(fnUtf)
(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
typed := uint8(0)
if ur.FileAttributes&win32api.FILE_ATTRIBUTE_DIRECTORY != 0 {
typed = 1
}
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn)
fileMap[ur.FileReferenceNumber] = FileEntry{Name: fn, Parent: ur.ParentFileReferenceNumber, Type: typed}
nextRef, err := parseUSNOutput(data, done, func(record usnRecordData) error {
fileMap[record.FileReferenceNumber] = mergeUSNFileEntry(fileMap[record.FileReferenceNumber], fileEntryFromUSNRecord(record))
return nil
})
if err != nil {
return fileMap, err
}
med.StartFileReferenceNumber = win32api.DWORDLONG(usn)
med.StartFileReferenceNumber = win32api.DWORDLONG(nextRef)
}
}
func filterUSNFileMap(fileMap map[win32api.DWORDLONG]FileEntry, searchFn func(string, bool) bool) map[win32api.DWORDLONG]FileEntry {
if searchFn == nil {
return fileMap
}
filtered := make(map[win32api.DWORDLONG]FileEntry)
for id, entry := range fileMap {
if entry.Type == 1 || searchFn(entry.Name, entry.Type == 1) {
filtered[id] = entry
}
}
return filtered
}
func ListUsnFile(driver string) (map[win32api.DWORDLONG]FileEntry, error) {
return buildUSNFileMap(driver)
}
func ListUsnFileFn(driver string, searchFn func(string, bool) bool) (map[win32api.DWORDLONG]FileEntry, error) {
fileMap := make(map[win32api.DWORDLONG]FileEntry)
pDriver := "\\\\.\\" + driver[:len(driver)-1]
fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
fileMap, err := buildUSNFileMap(driver)
if err != nil {
return fileMap, err
}
ujd, _, err := queryUsnJournal(fd)
if err != nil {
return fileMap, err
}
med := win32api.MFT_ENUM_DATA{0, 0, ujd.NextUsn}
for {
data, done, err := enumUsnData(fd, &med)
if err != nil && done != 0 {
return fileMap, err
}
if done == 0 {
return fileMap, nil
}
var usn win32api.USN = *(*win32api.USN)(unsafe.Pointer(&data[0]))
// fmt.Println("usn", usn)
var ur *win32api.USN_RECORD
for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
ur = (*win32api.USN_RECORD)(unsafe.Pointer(&data[i]))
nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
fnUtf := (*[10000]uint16)(fnp)[:nameLength]
fn := syscall.UTF16ToString(fnUtf)
(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
typed := uint8(0)
if ur.FileAttributes&win32api.FILE_ATTRIBUTE_DIRECTORY != 0 {
typed = 1
}
if typed == 1 || searchFn(fn, typed == 1) {
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn)
fileMap[ur.FileReferenceNumber] = FileEntry{Name: fn, Parent: ur.ParentFileReferenceNumber, Type: typed}
}
}
med.StartFileReferenceNumber = win32api.DWORDLONG(usn)
}
return filterUSNFileMap(fileMap, searchFn), nil
}
func GetFullUsnPath(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG) (name string) {
func buildUSNPath(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG) (name string) {
normalized, err := normalizeDiskName(diskName)
if err != nil {
return ""
}
for id != 0 {
fe := fileMap[id]
if id == fe.Parent {
@@ -281,32 +491,139 @@ func GetFullUsnPath(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, i
}
id = fe.Parent
}
name = diskName[:len(diskName)-1] + name
name = strings.TrimSuffix(normalized, "\\") + name
return
}
func GetFullUsnPathEntry(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, en FileMonitor) (name string) {
fileMap[en.Self] = FileEntry{
func normalizeExistingLongPath(path string) string {
if path == "" {
return path
}
if normalized, ok := getLongPathName(path); ok {
return trimLongPathPrefix(normalized)
}
longPath := fixLongPath(path)
if longPath == path {
return path
}
if normalized, ok := getLongPathName(longPath); ok {
return trimLongPathPrefix(normalized)
}
return path
}
func getLongPathName(path string) (string, bool) {
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return "", false
}
size := len(path) + 1
if size < syscall.MAX_PATH {
size = syscall.MAX_PATH
}
for {
buf := make([]uint16, size)
n, err := syscall.GetLongPathName(pathp, &buf[0], uint32(len(buf)))
if err != nil || n == 0 {
return "", false
}
if int(n) < len(buf) {
return syscall.UTF16ToString(buf[:n]), true
}
size = int(n) + 1
}
}
func trimLongPathPrefix(path string) string {
switch {
case strings.HasPrefix(path, `\\?\UNC\`):
return `\\` + path[len(`\\?\UNC\`):]
case strings.HasPrefix(path, `\\?\`):
return path[len(`\\?\`):]
default:
return path
}
}
func GetFullUsnPath(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG) string {
return normalizeExistingLongPath(buildUSNPath(diskName, fileMap, id))
}
func GetFullUsnPathEntry(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, en FileMonitor) string {
fileMap[en.Self] = mergeUSNFileEntry(fileMap[en.Self], FileEntry{
Name: en.Name,
Parent: en.Parent,
Type: en.Type,
})
return normalizeExistingLongPath(buildUSNPath(diskName, fileMap, en.Self))
}
func fileStatFromHandle(fd syscall.Handle, name string, path string) (FileStat, error) {
var info syscall.ByHandleFileInformation
if err := syscall.GetFileInformationByHandle(fd, &info); err != nil {
return FileStat{}, err
}
id := en.Self
for id != 0 {
fe := fileMap[id]
if id == fe.Parent {
name = "\\" + name
break
}
if name == "" {
name = fe.Name
} else {
name = fe.Name + "\\" + name
}
id = fe.Parent
stat := newFileStatFromInformation(&info, name, path)
fileType, err := syscall.GetFileType(fd)
if err == nil {
stat.filetype = fileType
}
name = diskName[:len(diskName)-1] + name
return
return stat, nil
}
func fileStatFromPath(name string, path string) (FileStat, error) {
fileInfo, err := os.Stat(path)
if err != nil {
return FileStat{}, err
}
data, ok := fileInfo.Sys().(*syscall.Win32FileAttributeData)
if !ok {
return FileStat{}, fmt.Errorf("unexpected file info payload %T", fileInfo.Sys())
}
return FileStat{
name: name,
path: path,
FileAttributes: data.FileAttributes,
CreationTime: data.CreationTime,
LastAccessTime: data.LastAccessTime,
LastWriteTime: data.LastWriteTime,
FileSizeHigh: data.FileSizeHigh,
FileSizeLow: data.FileSizeLow,
}, nil
}
func fileOpenAttributes(entryType uint8) uint32 {
if entryType == 1 {
return win32api.FILE_FLAG_BACKUP_SEMANTICS
}
return win32api.FILE_ATTRIBUTE_NORMAL
}
func fileStatFromIDWithfd(volumeHandle syscall.Handle, id win32api.DWORDLONG, name string, path string, entryType uint8) (FileStat, error) {
fileHandle, err := OpenFileByIdWithfd(volumeHandle, id, syscall.O_RDONLY, fileOpenAttributes(entryType))
if err != nil {
return FileStat{}, err
}
defer syscall.Close(fileHandle)
return fileStatFromHandle(fileHandle, name, path)
}
func fileStatForEntryWithfd(volumeHandle syscall.Handle, diskName string, data map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG, entry FileEntry) (FileStat, error) {
path := GetFullUsnPath(diskName, data, id)
stat, err := fileStatFromIDWithfd(volumeHandle, id, entry.Name, path, entry.Type)
if err == nil {
return stat, nil
}
fallback, fallbackErr := fileStatFromPath(entry.Name, path)
if fallbackErr == nil {
return fallback, nil
}
return FileStat{}, fmt.Errorf("stat by id: %v; stat by path: %w", err, fallbackErr)
}
func fileStatForEntryByPath(diskName string, data map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG, entry FileEntry) (FileStat, error) {
path := GetFullUsnPath(diskName, data, id)
return fileStatFromPath(entry.Name, path)
}
const (
@@ -352,12 +669,7 @@ func listNTFSUsnDriverFiles(diskName string, fn func(string, bool) bool, data ma
result[i] = name
i++
}
(*reflect.SliceHeader)(unsafe.Pointer(&result)).Cap = i
(*reflect.SliceHeader)(unsafe.Pointer(&result)).Len = i
data = nil
data = make(map[win32api.DWORDLONG]FileEntry, 0)
runtime.GC()
return result, nil
return result[:i], nil
}
func ListNTFSUsnDriverInfoFn(diskName string, searchFn func(string, bool) bool) ([]FileStat, error) {
@@ -384,73 +696,67 @@ func ListNTFSUsnDriverInfo(diskName string, folder uint8) ([]FileStat, error) {
}, data)
}
func listNTFSUsnDriverInfo(diskName string, fn func(string, bool) bool, data map[win32api.DWORDLONG]FileEntry) ([]FileStat, error) {
//fmt.Println("finished 1")
pDriver := "\\\\.\\" + diskName[:len(diskName)-1]
fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
if err != nil {
return nil, err
type fileStatFetcher func(id win32api.DWORDLONG, entry FileEntry) (FileStat, error)
func collectUSNFileStats(data map[win32api.DWORDLONG]FileEntry, fn func(string, bool) bool, fetch fileStatFetcher) []FileStat {
if fetch == nil {
return []FileStat{}
}
defer syscall.Close(fd)
result := make([]FileStat, len(data))
i := int(0)
if fn == nil {
fn = func(string, bool) bool { return true }
}
resultCh := make(chan FileStat, len(data))
wg := stario.NewWaitGroup(100)
for k, v := range data {
if !fn(v.Name, v.Type == 1) {
for id, entry := range data {
if !fn(entry.Name, entry.Type == 1) {
continue
}
wg.Add(1)
go func(k win32api.DWORDLONG, v FileEntry, i int) {
go func(id win32api.DWORDLONG, entry FileEntry) {
defer wg.Done()
//now := time.Now().UnixNano()
/*
fd2, err := OpenFileByIdWithfd(fd, k, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
if err != nil {
return
}
//fmt.Println("cost", float64((time.Now().UnixNano()-now)/1000000))
var info syscall.ByHandleFileInformation
err = syscall.GetFileInformationByHandle(fd2, &info)
syscall.Close(fd2)
//fmt.Println("cost", float64((time.Now().UnixNano()-now)/1000000))
if err != nil {
return
}
*/
path := GetFullUsnPath(diskName, data, k)
fileInfo, err := os.Stat(path)
stat, err := fetch(id, entry)
if err != nil {
return
}
fs := fileInfo.Sys().(*syscall.Win32FileAttributeData)
stat := FileStat{
FileAttributes: fs.FileAttributes,
CreationTime: fs.CreationTime,
LastAccessTime: fs.LastAccessTime,
LastWriteTime: fs.LastWriteTime,
FileSizeHigh: fs.FileSizeHigh,
FileSizeLow: fs.FileSizeLow,
}
stat.name = v.Name
stat.path = path
return
result[i] = stat
//result[i] = newFileStatFromInformation(&info, v.Name, path)
}(k, v, i)
i++
resultCh <- stat
}(id, entry)
}
wg.Wait()
//fmt.Println("finished 2")
(*reflect.SliceHeader)(unsafe.Pointer(&result)).Cap = i
(*reflect.SliceHeader)(unsafe.Pointer(&result)).Len = i
data = nil
//data = make(map[win32api.DWORDLONG]FileEntry, 0)
runtime.GC()
return result, nil
close(resultCh)
result := make([]FileStat, 0, len(data))
for stat := range resultCh {
result = append(result, stat)
}
return result
}
func getUsnJournalReasonString(reason win32api.DWORD) (s string) {
func listNTFSUsnDriverInfo(diskName string, fn func(string, bool) bool, data map[win32api.DWORDLONG]FileEntry) ([]FileStat, error) {
pDriver, err := volumeDevicePath(diskName)
if err != nil {
return nil, err
}
fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
useByID := err == nil
if useByID {
defer syscall.Close(fd)
}
var fetch fileStatFetcher
if useByID {
fetch = func(id win32api.DWORDLONG, entry FileEntry) (FileStat, error) {
return fileStatForEntryWithfd(fd, diskName, data, id, entry)
}
} else {
fetch = func(id win32api.DWORDLONG, entry FileEntry) (FileStat, error) {
return fileStatForEntryByPath(diskName, data, id, entry)
}
}
return collectUSNFileStats(data, fn, fetch), nil
}
func USNReasonString(reason win32api.DWORD) (s string) {
var reasons = []string{
"DataOverwrite", // 0x00000001
"DataExtend", // 0x00000002
@@ -485,75 +791,84 @@ func getUsnJournalReasonString(reason win32api.DWORD) (s string) {
"0x40000000", // 0x40000000
"*Close*", // 0x80000000
}
for i := 0; reason != 0; {
for i := 0; reason != 0; i++ {
if i >= len(reasons) {
if s == "" {
return fmt.Sprintf("0x%08X", uint32(reason)<<uint(i))
}
return s + fmt.Sprintf(", 0x%08X", uint32(reason)<<uint(i))
}
if reason&1 == 1 {
s = s + ", " + reasons[i]
}
reason >>= 1
i++
}
return
}
func getUsnJournalReasonString(reason win32api.DWORD) string {
return USNReasonString(reason)
}
func MonitorUsnChange(driver string, rec chan FileMonitor) error {
pDriver := "\\\\.\\" + driver[:len(driver)-1]
pDriver, err := volumeDevicePath(driver)
if err != nil {
return err
}
fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
if err != nil {
return err
}
defer syscall.Close(fd)
ujd, _, err := queryUsnJournal(fd)
if err != nil {
return err
}
rujd := win32api.READ_USN_JOURNAL_DATA{ujd.NextUsn, 0xFFFFFFFF, 0, 0, 1, ujd.UsnJournalID}
cache := make(map[win32api.DWORDLONG]FileEntry)
for {
var usn win32api.USN
data, done, err := readUsnJournal(fd, &rujd)
if err != nil || done <= uint32(unsafe.Sizeof(usn)) {
if err != nil || done <= uint32(usnBufferHeaderSize) {
return err
}
usn = *(*win32api.USN)(unsafe.Pointer(&data[0]))
var ur *win32api.USN_RECORD
for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
ur = (*win32api.USN_RECORD)(unsafe.Pointer(&data[i]))
nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
fn := syscall.UTF16ToString((*[10000]uint16)(fnp)[:nameLength])
(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", getFullPath(folders, ur.ParentFileReferenceNumber), syscall.UTF16ToString(fn), getUsnJournalReasonString(ur.Reason))
typed := uint8(0)
if ur.FileAttributes&win32api.FILE_ATTRIBUTE_DIRECTORY != 0 {
typed = 1
}
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn)
rec <- FileMonitor{Name: fn, Parent: ur.ParentFileReferenceNumber, Type: typed, Self: ur.FileReferenceNumber, Reason: getUsnJournalReasonString(ur.Reason)}
nextUsn, err := parseUSNOutput(data, done, func(record usnRecordData) error {
entry := mergeUSNFileEntry(cache[record.FileReferenceNumber], fileEntryFromUSNRecord(record))
cache[record.FileReferenceNumber] = entry
rec <- FileMonitor{Name: entry.Name, Parent: entry.Parent, Type: entry.Type, Self: record.FileReferenceNumber, Reason: getUsnJournalReasonString(record.Reason)}
return nil
})
if err != nil {
return err
}
rujd.StartUsn = usn
if usn == 0 {
rujd.StartUsn = win32api.USN(nextUsn)
if nextUsn == 0 {
return nil
}
}
}
func GetUsnFileInfo(diskName string, fileMap map[win32api.DWORDLONG]FileEntry, id win32api.DWORDLONG) (FileStat, error) {
name := fileMap[id].Name
path := GetFullUsnPath(diskName, fileMap, id)
fd, err := OpenFileById(diskName, id, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
pDriver, err := volumeDevicePath(diskName)
if err != nil {
return FileStat{}, err
}
var info syscall.ByHandleFileInformation
err = syscall.GetFileInformationByHandle(fd, &info)
return newFileStatFromInformation(&info, name, path), err
volumeHandle, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
if err != nil {
return fileStatForEntryByPath(diskName, fileMap, id, fileMap[id])
}
defer syscall.Close(volumeHandle)
return fileStatForEntryWithfd(volumeHandle, diskName, fileMap, id, fileMap[id])
}
// Need a custom Open to work with backup_semantics
func OpenFileById(diskName string, id win32api.DWORDLONG, mode int, attrs uint32) (syscall.Handle, error) {
pDriver := "\\\\.\\" + diskName[:len(diskName)-1]
pDriver, err := volumeDevicePath(diskName)
if err != nil {
return syscall.InvalidHandle, err
}
fd, err := CreateFile(pDriver, syscall.O_RDONLY, win32api.FILE_ATTRIBUTE_NORMAL)
if err != nil {
return syscall.InvalidHandle, err
@@ -585,11 +900,10 @@ func OpenFileByIdWithfd(fd syscall.Handle, id win32api.DWORDLONG, mode int, attr
sa = makeInheritSa()
}
fid := win32api.FILE_ID_DESCRIPTOR{
DwSize: 16,
Type: 0,
DwSize: win32api.DWORD(unsafe.Sizeof(win32api.FILE_ID_DESCRIPTOR{})),
Type: win32api.FileIdType,
FileId: id,
}
fid.DwSize = win32api.DWORD(unsafe.Sizeof(fid))
h, e := win32api.OpenFileById(win32api.HANDLE(fd), &fid, win32api.DWORD(access),
win32api.DWORD(sharemode), sa, win32api.DWORD(attrs))
return syscall.Handle(h), e