diff --git a/internal/stat.go b/internal/stat.go index db6df9a..29cfbda 100644 --- a/internal/stat.go +++ b/internal/stat.go @@ -286,7 +286,7 @@ func find(dir string, filter func(info fs.FileInfo) bool) ([]*file, error) { if !entry.IsDir() { fileChan <- &file{ name: entry.Name(), - size: fileInfo.Size(), + size: diskSize(fileInfo, filepath.Join(dir, entry.Name())), } continue } diff --git a/internal/sys_darwin.go b/internal/sys_darwin.go index 1641757..ba2f017 100644 --- a/internal/sys_darwin.go +++ b/internal/sys_darwin.go @@ -16,6 +16,21 @@ package internal +import ( + "os" + "syscall" +) + func sysFilter(dir string) bool { return "/dev" != dir } + +// diskSize returns the actual number of bytes allocated on disk for the file, +// i.e. allocated blocks (st_blocks * 512), matching `du`'s default behavior. +// For sparse files this is smaller than the apparent logical size. +func diskSize(info os.FileInfo, _ string) int64 { + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + return stat.Blocks * 512 // st_blocks is always in 512-byte units (POSIX) + } + return info.Size() +} diff --git a/internal/sys_linux.go b/internal/sys_linux.go index af0c97e..f9142a3 100644 --- a/internal/sys_linux.go +++ b/internal/sys_linux.go @@ -16,6 +16,21 @@ package internal +import ( + "os" + "syscall" +) + func sysFilter(dir string) bool { return dir != "/proc" } + +// diskSize returns the actual number of bytes allocated on disk for the file, +// i.e. allocated blocks (st_blocks * 512), matching `du`'s default behavior. +// For sparse files this is smaller than the apparent logical size. +func diskSize(info os.FileInfo, _ string) int64 { + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + return stat.Blocks * 512 // st_blocks is always in 512-byte units (POSIX) + } + return info.Size() +} diff --git a/internal/sys_windows.go b/internal/sys_windows.go index 0bbfc38..0b7e5a4 100644 --- a/internal/sys_windows.go +++ b/internal/sys_windows.go @@ -16,6 +16,39 @@ package internal +import ( + "os" + "syscall" + "unsafe" +) + +var ( + modKernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetCompressedFileSizeW = modKernel32.NewProc("GetCompressedFileSizeW") +) + func sysFilter(_ string) bool { return true } + +// diskSize returns the actual number of bytes allocated on disk for the file +// via GetCompressedFileSizeW, matching `du`'s default behavior. For sparse or +// compressed files this excludes unallocated holes, so it is smaller than the +// apparent logical size returned by os.FileInfo.Size(). +func diskSize(info os.FileInfo, name string) int64 { + p, err := syscall.UTF16PtrFromString(name) + if err != nil { + return info.Size() + } + + var high uint32 + low, _, _ := procGetCompressedFileSizeW.Call( + uintptr(unsafe.Pointer(p)), + uintptr(unsafe.Pointer(&high)), + ) + if uint32(low) == 0xFFFFFFFF { // INVALID_FILE_SIZE → call failed + return info.Size() + } + + return int64(uint64(high)<<32 | uint64(uint32(low))) +}