完善 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:
+197
-110
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user