7e6cc73106
- 新增自启动幂等配置、统一错误语义、进程等待和进程树终止能力 - 增强服务生命周期管理,支持等待状态、重启、幂等创建和配置更新 - 新增 NTFS 卷索引、文件 ID 解析、文件遍历、USN 变更监听和 bookmark 持久化 - 修复 NTFS boot sector、fragment、MFT、USN 解析边界和路径重建问题 - 补充权限、进程、服务、NTFS 解析和工作流回归测试 - 增加 Windows 测试脚本和管理员 NTFS smoke 验证脚本 - 升级 Go 兼容版本到 1.18,并更新 stario、win32api 及相关间接依赖
329 lines
7.7 KiB
Go
329 lines
7.7 KiB
Go
package mft
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type MFTFile struct {
|
|
Name string
|
|
Path string
|
|
ModTime time.Time
|
|
Size uint64
|
|
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) {
|
|
reader, size, recordSize, err := openMFTFile(driver)
|
|
if err != nil {
|
|
return []MFTFile{}, err
|
|
}
|
|
defer reader.Close()
|
|
|
|
return collectMFTFiles(driver, reader, size, recordSize, fn)
|
|
}
|
|
|
|
func GetFileListsByMft(driver string) ([]MFTFile, error) {
|
|
return GetFileListsByMftFn(driver, func(string, bool) bool { return true })
|
|
}
|
|
|
|
func GetFileListsFromMftFileFn(filepath string, fn func(string, bool) bool) ([]MFTFile, error) {
|
|
f, err := os.Open(filepath)
|
|
if err != nil {
|
|
return []MFTFile{}, err
|
|
}
|
|
defer f.Close()
|
|
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
return []MFTFile{}, err
|
|
}
|
|
|
|
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]
|
|
if id == fe.Parent {
|
|
name = "\\" + name
|
|
break
|
|
}
|
|
if name == "" {
|
|
name = fe.Name
|
|
} else {
|
|
name = fe.Name + "\\" + name
|
|
}
|
|
id = fe.Parent
|
|
}
|
|
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
|
|
}
|