完善 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
+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)
}