Skip to content

Instantly share code, notes, and snippets.

@pkrnjevic
Last active March 24, 2024 10:15
Show Gist options
  • Save pkrnjevic/7219861 to your computer and use it in GitHub Desktop.
Save pkrnjevic/7219861 to your computer and use it in GitHub Desktop.
Windows USN Journal sample in Go based on Jeffrey Richter's superb MSDN Journal article. A work in progress, intended to provide similar API to go.fsevents.
//
// File: fsevents_windows.go
// Date: October 29, 2013
// Author: Peter Krnjevic <[email protected]>, on the shoulders of many others
//
// This code sample is released into the Public Domain.
//
package fsevents
import (
// "bytes"
// "encoding/binary"
"fmt"
"reflect"
"syscall"
"unsafe"
)
import (
// "github.com/lxn/go-winapi"
"github.com/lxn/walk"
)
type (
WCHAR uint16
WORD uint16
DWORD uint32
DWORDLONG uint64
LONGLONG int64
USN int64
LARGE_INTEGER LONGLONG
)
type USN_JOURNAL_DATA struct {
UsnJournalID DWORDLONG
FirstUsn USN
NextUsn USN
LowestValidUsn USN
MaxUsn USN
MaximumSize DWORDLONG
AllocationDelta DWORDLONG
}
type READ_USN_JOURNAL_DATA struct {
StartUsn USN
ReasonMask DWORD
ReturnOnlyOnClose DWORD
Timeout DWORDLONG
BytesToWaitFor DWORDLONG
UsnJournalID DWORDLONG
}
type USN_RECORD struct {
RecordLength DWORD
MajorVersion WORD
MinorVersion WORD
FileReferenceNumber DWORDLONG
ParentFileReferenceNumber DWORDLONG
Usn USN
TimeStamp LARGE_INTEGER
Reason DWORD
SourceInfo DWORD
SecurityId DWORD
FileAttributes DWORD
FileNameLength WORD
FileNameOffset WORD
FileName [1]WCHAR
}
type MFT_ENUM_DATA struct {
StartFileReferenceNumber DWORDLONG
LowUsn USN
HighUsn USN
}
const (
FSCTL_ENUM_USN_DATA = 0x900B3
FSCTL_QUERY_USN_JOURNAL = 0x900F4
FSCTL_READ_USN_JOURNAL = 0x900BB
O_RDONLY = syscall.O_RDONLY
O_RDWR = syscall.O_RDWR
O_CREAT = syscall.O_CREAT
O_WRONLY = syscall.O_WRONLY
GENERIC_READ = syscall.GENERIC_READ
GENERIC_WRITE = syscall.GENERIC_WRITE
FILE_APPEND_DATA = syscall.FILE_APPEND_DATA
FILE_SHARE_READ = syscall.FILE_SHARE_READ
FILE_SHARE_WRITE = syscall.FILE_SHARE_WRITE
ERROR_FILE_NOT_FOUND = syscall.ERROR_FILE_NOT_FOUND
O_APPEND = syscall.O_APPEND
O_CLOEXEC = syscall.O_CLOEXEC
O_EXCL = syscall.O_EXCL
O_TRUNC = syscall.O_TRUNC
CREATE_ALWAYS = syscall.CREATE_ALWAYS
CREATE_NEW = syscall.CREATE_NEW
OPEN_ALWAYS = syscall.OPEN_ALWAYS
TRUNCATE_EXISTING = syscall.TRUNCATE_EXISTING
OPEN_EXISTING = syscall.OPEN_EXISTING
FILE_ATTRIBUTE_NORMAL = syscall.FILE_ATTRIBUTE_NORMAL
FILE_FLAG_BACKUP_SEMANTICS = syscall.FILE_FLAG_BACKUP_SEMANTICS
FILE_ATTRIBUTE_DIRECTORY = syscall.FILE_ATTRIBUTE_DIRECTORY
MAX_LONG_PATH = syscall.MAX_LONG_PATH
)
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
usnJournalData USN_JOURNAL_DATA
readUsnJournalData READ_USN_JOURNAL_DATA
cb int
)
func getPointer(i interface{}) (pointer, size uintptr) {
v := reflect.ValueOf(i)
switch k := v.Kind(); k {
case reflect.Ptr:
t := v.Elem().Type()
size = t.Size()
pointer = v.Pointer()
case reflect.Slice:
size = uintptr(v.Cap())
pointer = v.Pointer()
default:
fmt.Println("oops")
}
return
}
func DeviceIoControl(handle syscall.Handle, controlCode uint32, in interface{}, out interface{}, done *uint32) (err error) {
inPtr, inSize := getPointer(in)
outPtr, outSize := getPointer(out)
r1, _, e1 := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), uintptr(controlCode), inPtr, uintptr(inSize), outPtr, uintptr(outSize), uintptr(unsafe.Pointer(done)), uintptr(0), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func makeInheritSa() *syscall.SecurityAttributes {
var sa syscall.SecurityAttributes
sa.Length = uint32(unsafe.Sizeof(sa))
sa.InheritHandle = 1
return &sa
}
// Need a custom Open to work with backup_semantics
func open(path string, mode int, attrs uint32) (fd syscall.Handle, err error) {
if len(path) == 0 {
return syscall.InvalidHandle, ERROR_FILE_NOT_FOUND
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return syscall.InvalidHandle, err
}
var access uint32
switch mode & (O_RDONLY | O_WRONLY | O_RDWR) {
case O_RDONLY:
access = GENERIC_READ
case O_WRONLY:
access = GENERIC_WRITE
case O_RDWR:
access = GENERIC_READ | GENERIC_WRITE
}
if mode&O_CREAT != 0 {
access |= GENERIC_WRITE
}
if mode&O_APPEND != 0 {
access &^= GENERIC_WRITE
access |= FILE_APPEND_DATA
}
sharemode := uint32(FILE_SHARE_READ | FILE_SHARE_WRITE)
var sa *syscall.SecurityAttributes
if mode&O_CLOEXEC == 0 {
sa = makeInheritSa()
}
var createmode uint32
switch {
case mode&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL):
createmode = CREATE_NEW
case mode&(O_CREAT|O_TRUNC) == (O_CREAT | O_TRUNC):
createmode = CREATE_ALWAYS
case mode&O_CREAT == O_CREAT:
createmode = OPEN_ALWAYS
case mode&O_TRUNC == O_TRUNC:
createmode = TRUNCATE_EXISTING
default:
createmode = OPEN_EXISTING
}
h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
return h, e
}
func getUsnJournalReasonString(reason DWORD) (s string) {
var reasons = []string{
"DataOverwrite", // 0x00000001
"DataExtend", // 0x00000002
"DataTruncation", // 0x00000004
"0x00000008", // 0x00000008
"NamedDataOverwrite", // 0x00000010
"NamedDataExtend", // 0x00000020
"NamedDataTruncation", // 0x00000040
"0x00000080", // 0x00000080
"FileCreate", // 0x00000100
"FileDelete", // 0x00000200
"PropertyChange", // 0x00000400
"SecurityChange", // 0x00000800
"RenameOldName", // 0x00001000
"RenameNewName", // 0x00002000
"IndexableChange", // 0x00004000
"BasicInfoChange", // 0x00008000
"HardLinkChange", // 0x00010000
"CompressionChange", // 0x00020000
"EncryptionChange", // 0x00040000
"ObjectIdChange", // 0x00080000
"ReparsePointChange", // 0x00100000
"StreamChange", // 0x00200000
"0x00400000", // 0x00400000
"0x00800000", // 0x00800000
"0x01000000", // 0x01000000
"0x02000000", // 0x02000000
"0x04000000", // 0x04000000
"0x08000000", // 0x08000000
"0x10000000", // 0x10000000
"0x20000000", // 0x20000000
"0x40000000", // 0x40000000
"*Close*", // 0x80000000
}
for i := 0; reason != 0; {
if reason&1 == 1 {
s = s + ", " + reasons[i]
}
reason >>= 1
i++
}
return
}
// Query usn journal data
func queryUsnJournal(fd syscall.Handle) (ujd USN_JOURNAL_DATA, done uint32, err error) {
err = DeviceIoControl(fd, FSCTL_QUERY_USN_JOURNAL, []byte{}, &ujd, &done)
return
}
func readUsnJournal(fd syscall.Handle, rujd *READ_USN_JOURNAL_DATA) (data []byte, done uint32, err error) {
data = make([]byte, 0x1000)
err = DeviceIoControl(fd, FSCTL_READ_USN_JOURNAL, rujd, data, &done)
return
}
func enumUsnData(fd syscall.Handle, med *MFT_ENUM_DATA) (data []byte, done uint32, err error) {
data = make([]byte, 0x10000)
err = DeviceIoControl(fd, FSCTL_ENUM_USN_DATA, med, data, &done)
return
}
type folderEntry struct {
name string
parent DWORDLONG
}
// Build map of folder names using MFT (based on PopulateMethod2)
func buildFolderMap() (folders map[DWORDLONG]folderEntry) {
folders = make(map[DWORDLONG]folderEntry)
drives, _ := walk.DriveNames()
fmt.Println(drives)
fd, err := open("\\\\.\\C:", syscall.O_RDONLY, FILE_ATTRIBUTE_NORMAL)
fmt.Println(fd, err)
ujd, _, err := queryUsnJournal(fd)
fmt.Printf("ujd = %v\n", ujd)
// Open directory to read MFT and store off FRN (file reference numbers)
dir, err := open("C:\\", syscall.O_RDONLY, FILE_FLAG_BACKUP_SEMANTICS)
fmt.Println("dir,err", dir, err)
var fi syscall.ByHandleFileInformation
err = syscall.GetFileInformationByHandle(dir, &fi)
err = syscall.CloseHandle(dir)
fmt.Println("err, fi", err, fi)
indexRoot := fi.FileSizeHigh<<32 | fi.FileSizeLow
_ = indexRoot
med := MFT_ENUM_DATA{0, 0, ujd.NextUsn}
for {
data, done, err := enumUsnData(fd, &med)
if err != nil {
fmt.Println(err)
}
if done == 0 {
return
}
var usn USN = *(*USN)(unsafe.Pointer(&data[0]))
// fmt.Println("usn", usn)
var ur *USN_RECORD
for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
ur = (*USN_RECORD)(unsafe.Pointer(&data[i]))
if ur.FileAttributes&FILE_ATTRIBUTE_DIRECTORY != 0 {
nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
fnUtf := (*[10000]uint16)(fnp)[:nameLength]
fn := syscall.UTF16ToString(fnUtf)
(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", fn)
folders[ur.FileReferenceNumber] = folderEntry{fn, ur.ParentFileReferenceNumber}
}
}
med.StartFileReferenceNumber = DWORDLONG(usn)
}
}
// Assemble the path by looking up parent pointers in the folders map
func getFullPath(folders map[DWORDLONG]folderEntry, parent DWORDLONG) (name string) {
for parent != 0 {
fe := folders[parent]
name = fe.name + "/" + name
parent = fe.parent
}
return
}
func processAvailableRecords(ch chan *USN_RECORD, folders map[DWORDLONG]folderEntry) {
drives, _ := walk.DriveNames()
fmt.Println(drives)
fd, err := open("\\\\.\\C:", syscall.O_RDONLY, FILE_ATTRIBUTE_NORMAL)
fmt.Println("fd, err", fd, err)
ujd, _, err := queryUsnJournal(fd)
fmt.Printf("ujd = %v\n", ujd)
rujd := READ_USN_JOURNAL_DATA{ujd.FirstUsn, 0xFFFFFFFF, 0, 0, 1, ujd.UsnJournalID}
for {
var usn USN
data, done, err := readUsnJournal(fd, &rujd)
if err != nil || done <= uint32(unsafe.Sizeof(usn)) {
return
}
usn = *(*USN)(unsafe.Pointer(&data[0]))
fmt.Println("usn", usn)
var ur *USN_RECORD
for i := unsafe.Sizeof(usn); i < uintptr(done); i += uintptr(ur.RecordLength) {
ur = (*USN_RECORD)(unsafe.Pointer(&data[i]))
if ur.FileAttributes&FILE_ATTRIBUTE_DIRECTORY != 0 {
nameLength := uintptr(ur.FileNameLength) / unsafe.Sizeof(ur.FileName[0])
fnp := unsafe.Pointer(&data[i+uintptr(ur.FileNameOffset)])
fn := (*[10000]uint16)(fnp)[:nameLength]
(*reflect.SliceHeader)(unsafe.Pointer(&fn)).Cap = int(nameLength)
// fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", getFullPath(folders, ur.ParentFileReferenceNumber), syscall.UTF16ToString(fn), getUsnJournalReasonString(ur.Reason))
ch <- ur
}
}
rujd.StartUsn = usn
if usn == 0 {
return
}
}
}
// Main
func main() {
folders := buildFolderMap()
ch := make(chan *USN_RECORD)
go processAvailableRecords(ch, folders)
for {
ur := <-ch
fmt.Println("len", ur.FileNameLength, ur.FileNameOffset, "fn", getFullPath(folders, ur.ParentFileReferenceNumber), getUsnJournalReasonString(ur.Reason))
// fmt.Println(r)
}
}
type PathEvent struct {
Path string
Flags uint32
Eid uint64
}
func WatchPaths(paths []string, eid int64) chan []PathEvent {
}
func Unwatch(ch chan []PathEvent) {
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment