Files
go-winio/wim/wim.go
T
Hamza El-Saawy e268c11e27 Add lint and go generate steps to CI (#254)
* Add lint and go generate stages to CI

Add CI step to verify `go generate` was run on repo.
Add linter stage to CI along with linter config file,
`.golangci.yml`.
Will likely prefer revive over static-check.

Updated README Contributing section on linting requirements.

Added sequence ordering to make sure lint and go generate stages run
before tests and build.
This way, build and tests are not run on code that could potentially:

    1. not build due to `gofmt` issues;
    2. contain bugs;
    3. have to be re-submitted after issues are fixed; or
    4. contain outdated Win32 syscall or other auto-generated files.

Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com>

* Fixed linter issues

Code changes to satisfy linters:

 - Ran `gofmt -s -w` on repo.
 - Broke up long lines.
 - When possible, changed names with incorrect initialism formatting
   - Added exceptions for exported variables.
 - Added exceptions for ALL_CAPS_WITH_UNDERSCORES code.
   - Switched to using `windows` or `syscall` definitions if possible;
     especially if some constants were unused.
 - Added `_ =` to satisfy error linter, and acknowledge that errors are
   being ignored.
 - Switched to using `errors.Is` and `As` in places, elsewhere added
   exceptions if error value was known to be `syscall.Errno`.
 - Removed bare returns.
 - Prevented variables from being overshadowed in certain places
   (ignoring cases of overshadowing `err`).
 - Renamed variables and functions (eg, `len`, `eventMetadata.bytes`) to
   prevent shadowing pre-built functions and imported pacakges.
 - Removed unused method receivers.
 - Added exceptions to certain unused (unexported) constants and
   functions.
   - Deleted unused `once` from `pkg/etw.providerMap`.
 - Renamed `noop.go` files to `main_other.go` or `doc.go`, to better fit
   style recommendations.
 - Added exceptions for non-secure use of SHA1 and weak crypto
   libraries.
 - Replaced `ioutil` with `io` and `os` (and `t.TempDir` in tests).
 - Added fully exhaustive checks for `switch` statements in `pkg/etw`.
 - Defined constant strings for `tools/mkwinsyscall`.
 - Removed unnecessary conversions.
 - Made sure `context.Cancel` was called.

Additionally, added `//go:build windows" constraints on files with
unexported code, since linter will complain about unused code on
non-Windows platforms.

Added a stub `main() {}` for `mkwinsyscall` for non-Windows builds, just in
case `//go:generate` directives are added to OS-agnostic files.

Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com>

* PR: spelling, constants, fuzzing

Moved HVSocket fuzzing tests to separate file with go 1.18 build
constraint.

Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com>

Signed-off-by: Hamza El-Saawy <hamzaelsaawy@microsoft.com>
2022-08-23 15:05:05 -04:00

899 lines
22 KiB
Go

//go:build windows || linux
// +build windows linux
// Package wim implements a WIM file parser.
//
// WIM files are used to distribute Windows file system and container images.
// They are documented at https://msdn.microsoft.com/en-us/library/windows/desktop/dd861280.aspx.
package wim
import (
"bytes"
"crypto/sha1" //nolint:gosec // not used for secure application
"encoding/binary"
"encoding/xml"
"errors"
"fmt"
"io"
"strconv"
"sync"
"time"
"unicode/utf16"
)
// File attribute constants from Windows.
//
//nolint:revive // var-naming: ALL_CAPS
const (
FILE_ATTRIBUTE_READONLY = 0x00000001
FILE_ATTRIBUTE_HIDDEN = 0x00000002
FILE_ATTRIBUTE_SYSTEM = 0x00000004
FILE_ATTRIBUTE_DIRECTORY = 0x00000010
FILE_ATTRIBUTE_ARCHIVE = 0x00000020
FILE_ATTRIBUTE_DEVICE = 0x00000040
FILE_ATTRIBUTE_NORMAL = 0x00000080
FILE_ATTRIBUTE_TEMPORARY = 0x00000100
FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
FILE_ATTRIBUTE_COMPRESSED = 0x00000800
FILE_ATTRIBUTE_OFFLINE = 0x00001000
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000
FILE_ATTRIBUTE_ENCRYPTED = 0x00004000
FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000
FILE_ATTRIBUTE_VIRTUAL = 0x00010000
FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000
FILE_ATTRIBUTE_EA = 0x00040000
)
// Windows processor architectures.
//
//nolint:revive // var-naming: ALL_CAPS
const (
PROCESSOR_ARCHITECTURE_INTEL = 0
PROCESSOR_ARCHITECTURE_MIPS = 1
PROCESSOR_ARCHITECTURE_ALPHA = 2
PROCESSOR_ARCHITECTURE_PPC = 3
PROCESSOR_ARCHITECTURE_SHX = 4
PROCESSOR_ARCHITECTURE_ARM = 5
PROCESSOR_ARCHITECTURE_IA64 = 6
PROCESSOR_ARCHITECTURE_ALPHA64 = 7
PROCESSOR_ARCHITECTURE_MSIL = 8
PROCESSOR_ARCHITECTURE_AMD64 = 9
PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 = 10
PROCESSOR_ARCHITECTURE_NEUTRAL = 11
PROCESSOR_ARCHITECTURE_ARM64 = 12
)
var wimImageTag = [...]byte{'M', 'S', 'W', 'I', 'M', 0, 0, 0}
// todo: replace this with pkg/guid.GUID (and add tests to make sure nothing breaks)
type guid struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
func (g guid) String() string {
return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
g.Data1,
g.Data2,
g.Data3,
g.Data4[0],
g.Data4[1],
g.Data4[2],
g.Data4[3],
g.Data4[4],
g.Data4[5],
g.Data4[6],
g.Data4[7])
}
type resourceDescriptor struct {
FlagsAndCompressedSize uint64
Offset int64
OriginalSize int64
}
type resFlag byte
//nolint:deadcode,varcheck // need unused variables for iota to work
const (
resFlagFree resFlag = 1 << iota
resFlagMetadata
resFlagCompressed
resFlagSpanned
)
const validate = false
const supportedResFlags = resFlagMetadata | resFlagCompressed
func (r *resourceDescriptor) Flags() resFlag {
return resFlag(r.FlagsAndCompressedSize >> 56)
}
func (r *resourceDescriptor) CompressedSize() int64 {
return int64(r.FlagsAndCompressedSize & 0xffffffffffffff)
}
func (r *resourceDescriptor) String() string {
s := fmt.Sprintf("%d bytes at %d", r.CompressedSize(), r.Offset)
if r.Flags()&4 != 0 {
s += fmt.Sprintf(" (uncompresses to %d)", r.OriginalSize)
}
return s
}
// SHA1Hash contains the SHA1 hash of a file or stream.
type SHA1Hash [20]byte
type streamDescriptor struct {
resourceDescriptor
PartNumber uint16
RefCount uint32
Hash SHA1Hash
}
type hdrFlag uint32
//nolint:deadcode,varcheck // need unused variables for iota to work
const (
hdrFlagReserved hdrFlag = 1 << iota
hdrFlagCompressed
hdrFlagReadOnly
hdrFlagSpanned
hdrFlagResourceOnly
hdrFlagMetadataOnly
hdrFlagWriteInProgress
hdrFlagRpFix
)
//nolint:deadcode,varcheck // need unused variables for iota to work
const (
hdrFlagCompressReserved hdrFlag = 1 << (iota + 16)
hdrFlagCompressXpress
hdrFlagCompressLzx
)
const supportedHdrFlags = hdrFlagRpFix | hdrFlagReadOnly | hdrFlagCompressed | hdrFlagCompressLzx
type wimHeader struct {
ImageTag [8]byte
Size uint32
Version uint32
Flags hdrFlag
CompressionSize uint32
WIMGuid guid
PartNumber uint16
TotalParts uint16
ImageCount uint32
OffsetTable resourceDescriptor
XMLData resourceDescriptor
BootMetadata resourceDescriptor
BootIndex uint32
Padding uint32
Integrity resourceDescriptor
Unused [60]byte
}
type securityblockDisk struct {
TotalLength uint32
NumEntries uint32
}
const securityblockDiskSize = 8
type direntry struct {
Attributes uint32
SecurityID uint32
SubdirOffset int64
Unused1, Unused2 int64
CreationTime Filetime
LastAccessTime Filetime
LastWriteTime Filetime
Hash SHA1Hash
Padding uint32
ReparseHardLink int64
StreamCount uint16
ShortNameLength uint16
FileNameLength uint16
}
var direntrySize = int64(binary.Size(direntry{}) + 8) // includes an 8-byte length prefix
type streamentry struct {
Unused int64
Hash SHA1Hash
NameLength int16
}
var streamentrySize = int64(binary.Size(streamentry{}) + 8) // includes an 8-byte length prefix
// Filetime represents a Windows time.
type Filetime struct {
LowDateTime uint32
HighDateTime uint32
}
// Time returns the time as time.Time.
func (ft *Filetime) Time() time.Time {
// 100-nanosecond intervals since January 1, 1601
nsec := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime)
// change starting time to the Epoch (00:00:00 UTC, January 1, 1970)
nsec -= 116444736000000000
// convert into nanoseconds
nsec *= 100
return time.Unix(0, nsec)
}
// UnmarshalXML unmarshalls the time from a WIM XML blob.
func (ft *Filetime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Time struct {
Low string `xml:"LOWPART"`
High string `xml:"HIGHPART"`
}
var t Time
err := d.DecodeElement(&t, &start)
if err != nil {
return err
}
low, err := strconv.ParseUint(t.Low, 0, 32)
if err != nil {
return err
}
high, err := strconv.ParseUint(t.High, 0, 32)
if err != nil {
return err
}
ft.LowDateTime = uint32(low)
ft.HighDateTime = uint32(high)
return nil
}
type info struct {
Image []ImageInfo `xml:"IMAGE"`
}
// ImageInfo contains information about the image.
type ImageInfo struct {
Name string `xml:"NAME"`
Index int `xml:"INDEX,attr"`
CreationTime Filetime `xml:"CREATIONTIME"`
ModTime Filetime `xml:"LASTMODIFICATIONTIME"`
Windows *WindowsInfo `xml:"WINDOWS"`
}
// WindowsInfo contains information about the Windows installation in the image.
type WindowsInfo struct {
Arch byte `xml:"ARCH"`
ProductName string `xml:"PRODUCTNAME"`
EditionID string `xml:"EDITIONID"`
InstallationType string `xml:"INSTALLATIONTYPE"`
ProductType string `xml:"PRODUCTTYPE"`
Languages []string `xml:"LANGUAGES>LANGUAGE"`
DefaultLanguage string `xml:"LANGUAGES>DEFAULT"`
Version Version `xml:"VERSION"`
SystemRoot string `xml:"SYSTEMROOT"`
}
// Version represents a Windows build version.
type Version struct {
Major int `xml:"MAJOR"`
Minor int `xml:"MINOR"`
Build int `xml:"BUILD"`
SPBuild int `xml:"SPBUILD"`
SPLevel int `xml:"SPLEVEL"`
}
// ParseError is returned when the WIM cannot be parsed.
type ParseError struct {
Oper string
Path string
Err error
}
func (e *ParseError) Error() string {
if e.Path == "" {
return "WIM parse error at " + e.Oper + ": " + e.Err.Error()
}
return fmt.Sprintf("WIM parse error: %s %s: %s", e.Oper, e.Path, e.Err.Error())
}
func (e *ParseError) Unwrap() error { return e.Err }
// Reader provides functions to read a WIM file.
type Reader struct {
hdr wimHeader
r io.ReaderAt
fileData map[SHA1Hash]resourceDescriptor
XMLInfo string // The XML information about the WIM.
Image []*Image // The WIM's images.
}
// Image represents an image within a WIM file.
type Image struct {
wim *Reader
offset resourceDescriptor
sds [][]byte
rootOffset int64
r io.ReadCloser
curOffset int64
m sync.Mutex
ImageInfo
}
// StreamHeader contains alternate data stream metadata.
type StreamHeader struct {
Name string
Hash SHA1Hash
Size int64
}
// Stream represents an alternate data stream or reparse point data stream.
type Stream struct {
StreamHeader
wim *Reader
offset resourceDescriptor
}
// FileHeader contains file metadata.
type FileHeader struct {
Name string
ShortName string
Attributes uint32
SecurityDescriptor []byte
CreationTime Filetime
LastAccessTime Filetime
LastWriteTime Filetime
Hash SHA1Hash
Size int64
LinkID int64
ReparseTag uint32
ReparseReserved uint32
}
// File represents a file or directory in a WIM image.
type File struct {
FileHeader
Streams []*Stream
offset resourceDescriptor
img *Image
subdirOffset int64
}
// NewReader returns a Reader that can be used to read WIM file data.
func NewReader(f io.ReaderAt) (*Reader, error) {
r := &Reader{r: f}
section := io.NewSectionReader(f, 0, 0xffff)
err := binary.Read(section, binary.LittleEndian, &r.hdr)
if err != nil {
return nil, err
}
if r.hdr.ImageTag != wimImageTag {
return nil, &ParseError{Oper: "image tag", Err: errors.New("not a WIM file")}
}
if r.hdr.Flags&^supportedHdrFlags != 0 {
return nil, fmt.Errorf("unsupported WIM flags %x", r.hdr.Flags&^supportedHdrFlags)
}
if r.hdr.CompressionSize != 0x8000 {
return nil, fmt.Errorf("unsupported compression size %d", r.hdr.CompressionSize)
}
if r.hdr.TotalParts != 1 {
return nil, errors.New("multi-part WIM not supported")
}
fileData, images, err := r.readOffsetTable(&r.hdr.OffsetTable)
if err != nil {
return nil, err
}
xmlinfo, err := r.readXML()
if err != nil {
return nil, err
}
var inf info
err = xml.Unmarshal([]byte(xmlinfo), &inf)
if err != nil {
return nil, &ParseError{Oper: "XML info", Err: err}
}
for i, img := range images {
for _, imgInfo := range inf.Image {
if imgInfo.Index == i+1 {
img.ImageInfo = imgInfo
break
}
}
}
r.fileData = fileData
r.Image = images
r.XMLInfo = xmlinfo
return r, nil
}
// Close releases resources associated with the Reader.
func (r *Reader) Close() error {
for _, img := range r.Image {
img.reset()
}
return nil
}
func (r *Reader) resourceReader(hdr *resourceDescriptor) (io.ReadCloser, error) {
return r.resourceReaderWithOffset(hdr, 0)
}
func (r *Reader) resourceReaderWithOffset(hdr *resourceDescriptor, offset int64) (io.ReadCloser, error) {
var sr io.ReadCloser
section := io.NewSectionReader(r.r, hdr.Offset, hdr.CompressedSize())
if hdr.Flags()&resFlagCompressed == 0 {
_, _ = section.Seek(offset, 0)
sr = io.NopCloser(section)
} else {
cr, err := newCompressedReader(section, hdr.OriginalSize, offset)
if err != nil {
return nil, err
}
sr = cr
}
return sr, nil
}
func (r *Reader) readResource(hdr *resourceDescriptor) ([]byte, error) {
rsrc, err := r.resourceReader(hdr)
if err != nil {
return nil, err
}
defer rsrc.Close()
return io.ReadAll(rsrc)
}
func (r *Reader) readXML() (string, error) {
if r.hdr.XMLData.CompressedSize() == 0 {
return "", nil
}
rsrc, err := r.resourceReader(&r.hdr.XMLData)
if err != nil {
return "", err
}
defer rsrc.Close()
xmlData := make([]uint16, r.hdr.XMLData.OriginalSize/2)
err = binary.Read(rsrc, binary.LittleEndian, xmlData)
if err != nil {
return "", &ParseError{Oper: "XML data", Err: err}
}
// The BOM will always indicate little-endian UTF-16.
if xmlData[0] != 0xfeff {
return "", &ParseError{Oper: "XML data", Err: errors.New("invalid BOM")}
}
return string(utf16.Decode(xmlData[1:])), nil
}
func (r *Reader) readOffsetTable(res *resourceDescriptor) (map[SHA1Hash]resourceDescriptor, []*Image, error) {
fileData := make(map[SHA1Hash]resourceDescriptor)
var images []*Image
offsetTable, err := r.readResource(res)
if err != nil {
return nil, nil, &ParseError{Oper: "offset table", Err: err}
}
br := bytes.NewReader(offsetTable)
for i := 0; ; i++ {
var res streamDescriptor
err := binary.Read(br, binary.LittleEndian, &res)
if err == io.EOF { //nolint:errorlint
break
}
if err != nil {
return nil, nil, &ParseError{Oper: "offset table", Err: err}
}
if res.Flags()&^supportedResFlags != 0 {
return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("unsupported resource flag")}
}
// Validation for ad-hoc testing
if validate {
sec, err := r.resourceReader(&res.resourceDescriptor)
if err != nil {
panic(fmt.Sprint(i, err))
}
hash := sha1.New() //nolint:gosec // not used for secure application
_, err = io.Copy(hash, sec)
sec.Close()
if err != nil {
panic(fmt.Sprint(i, err))
}
var cmphash SHA1Hash
copy(cmphash[:], hash.Sum(nil))
if cmphash != res.Hash {
panic(fmt.Sprint(i, "hash mismatch"))
}
}
if res.Flags()&resFlagMetadata != 0 {
image := &Image{
wim: r,
offset: res.resourceDescriptor,
}
images = append(images, image)
} else {
fileData[res.Hash] = res.resourceDescriptor
}
}
if len(images) != int(r.hdr.ImageCount) {
return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("mismatched image count")}
}
return fileData, images, nil
}
func (*Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, err error) {
var secBlock securityblockDisk
err = binary.Read(rsrc, binary.LittleEndian, &secBlock)
if err != nil {
return sds, 0, &ParseError{Oper: "security table", Err: err}
}
n += securityblockDiskSize
secSizes := make([]int64, secBlock.NumEntries)
err = binary.Read(rsrc, binary.LittleEndian, &secSizes)
if err != nil {
return sds, n, &ParseError{Oper: "security table sizes", Err: err}
}
n += int64(secBlock.NumEntries * 8)
sds = make([][]byte, secBlock.NumEntries)
for i, size := range secSizes {
sd := make([]byte, size&0xffffffff)
_, err = io.ReadFull(rsrc, sd)
if err != nil {
return sds, n, &ParseError{Oper: "security descriptor", Err: err}
}
n += int64(len(sd))
sds[i] = sd
}
secsize := int64((secBlock.TotalLength + 7) &^ 7)
if n > secsize {
return sds, n, &ParseError{Oper: "security descriptor", Err: errors.New("security descriptor table too small")}
}
_, err = io.CopyN(io.Discard, rsrc, secsize-n)
if err != nil {
return sds, n, err
}
n = secsize
return sds, n, nil
}
// Open parses the image and returns the root directory.
func (img *Image) Open() (*File, error) {
if img.sds == nil {
rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, img.rootOffset)
if err != nil {
return nil, err
}
sds, n, err := img.wim.readSecurityDescriptors(rsrc)
if err != nil {
rsrc.Close()
return nil, err
}
img.sds = sds
img.r = rsrc
img.rootOffset = n
img.curOffset = n
}
f, err := img.readdir(img.rootOffset)
if err != nil {
return nil, err
}
if len(f) != 1 {
return nil, &ParseError{Oper: "root directory", Err: errors.New("expected exactly 1 root directory entry")}
}
return f[0], err
}
func (img *Image) reset() {
if img.r != nil {
img.r.Close()
img.r = nil
}
img.curOffset = -1
}
func (img *Image) readdir(offset int64) ([]*File, error) {
img.m.Lock()
defer img.m.Unlock()
if offset < img.curOffset || offset > img.curOffset+chunkSize {
// Reset to seek backward or to seek forward very far.
img.reset()
}
if img.r == nil {
rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, offset)
if err != nil {
return nil, err
}
img.r = rsrc
img.curOffset = offset
}
if offset > img.curOffset {
_, err := io.CopyN(io.Discard, img.r, offset-img.curOffset)
if err != nil {
img.reset()
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, err
}
}
var entries []*File
for {
e, n, err := img.readNextEntry(img.r)
img.curOffset += n
if err == io.EOF { //nolint:errorlint
break
}
if err != nil {
img.reset()
return nil, err
}
entries = append(entries, e)
}
return entries, nil
}
func (img *Image) readNextEntry(r io.Reader) (*File, int64, error) {
var length int64
err := binary.Read(r, binary.LittleEndian, &length)
if err != nil {
return nil, 0, &ParseError{Oper: "directory length check", Err: err}
}
if length == 0 {
return nil, 8, io.EOF
}
left := length
if left < direntrySize {
return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short")}
}
var dentry direntry
err = binary.Read(r, binary.LittleEndian, &dentry)
if err != nil {
return nil, 0, &ParseError{Oper: "directory entry", Err: err}
}
left -= direntrySize
namesLen := int64(dentry.FileNameLength + 2 + dentry.ShortNameLength)
if left < namesLen {
return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short for names")}
}
names := make([]uint16, namesLen/2)
err = binary.Read(r, binary.LittleEndian, names)
if err != nil {
return nil, 0, &ParseError{Oper: "file name", Err: err}
}
left -= namesLen
var name, shortName string
if dentry.FileNameLength > 0 {
name = string(utf16.Decode(names[:dentry.FileNameLength/2]))
}
if dentry.ShortNameLength > 0 {
shortName = string(utf16.Decode(names[dentry.FileNameLength/2+1:]))
}
var offset resourceDescriptor
zerohash := SHA1Hash{}
if dentry.Hash != zerohash {
var ok bool
offset, ok = img.wim.fileData[dentry.Hash]
if !ok {
return nil, 0, &ParseError{
Oper: "directory entry",
Path: name,
Err: fmt.Errorf("could not find file data matching hash %#v", dentry),
}
}
}
f := &File{
FileHeader: FileHeader{
Attributes: dentry.Attributes,
CreationTime: dentry.CreationTime,
LastAccessTime: dentry.LastAccessTime,
LastWriteTime: dentry.LastWriteTime,
Hash: dentry.Hash,
Size: offset.OriginalSize,
Name: name,
ShortName: shortName,
},
offset: offset,
img: img,
subdirOffset: dentry.SubdirOffset,
}
isDir := false
if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT == 0 {
f.LinkID = dentry.ReparseHardLink
if dentry.Attributes&FILE_ATTRIBUTE_DIRECTORY != 0 {
isDir = true
}
} else {
f.ReparseTag = uint32(dentry.ReparseHardLink)
f.ReparseReserved = uint32(dentry.ReparseHardLink >> 32)
}
if isDir && f.subdirOffset == 0 {
return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("no subdirectory data for directory")}
} else if !isDir && f.subdirOffset != 0 {
return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("unexpected subdirectory data for non-directory")}
}
if dentry.SecurityID != 0xffffffff {
f.SecurityDescriptor = img.sds[dentry.SecurityID]
}
_, err = io.CopyN(io.Discard, r, left)
if err != nil {
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, 0, err
}
if dentry.StreamCount > 0 {
var streams []*Stream
for i := uint16(0); i < dentry.StreamCount; i++ {
s, n, err := img.readNextStream(r)
length += n
if err != nil {
return nil, 0, err
}
// The first unnamed stream should be treated as the file stream.
if i == 0 && s.Name == "" {
f.Hash = s.Hash
f.Size = s.Size
f.offset = s.offset
} else if s.Name != "" {
streams = append(streams, s)
}
}
f.Streams = streams
}
if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT != 0 && f.Size == 0 {
return nil, 0, &ParseError{
Oper: "directory entry",
Path: name,
Err: errors.New("reparse point is missing reparse stream"),
}
}
return f, length, nil
}
func (img *Image) readNextStream(r io.Reader) (*Stream, int64, error) {
var length int64
err := binary.Read(r, binary.LittleEndian, &length)
if err != nil {
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, 0, &ParseError{Oper: "stream length check", Err: err}
}
left := length
if left < streamentrySize {
return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short")}
}
var sentry streamentry
err = binary.Read(r, binary.LittleEndian, &sentry)
if err != nil {
return nil, 0, &ParseError{Oper: "stream entry", Err: err}
}
left -= streamentrySize
if left < int64(sentry.NameLength) {
return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short for name")}
}
names := make([]uint16, sentry.NameLength/2)
err = binary.Read(r, binary.LittleEndian, names)
if err != nil {
return nil, 0, &ParseError{Oper: "file name", Err: err}
}
left -= int64(sentry.NameLength)
name := string(utf16.Decode(names))
var offset resourceDescriptor
if sentry.Hash != (SHA1Hash{}) {
var ok bool
offset, ok = img.wim.fileData[sentry.Hash]
if !ok {
return nil, 0, &ParseError{
Oper: "stream entry",
Path: name,
Err: fmt.Errorf("could not find file data matching hash %v", sentry.Hash),
}
}
}
s := &Stream{
StreamHeader: StreamHeader{
Hash: sentry.Hash,
Size: offset.OriginalSize,
Name: name,
},
wim: img.wim,
offset: offset,
}
_, err = io.CopyN(io.Discard, r, left)
if err != nil {
if err == io.EOF { //nolint:errorlint
err = io.ErrUnexpectedEOF
}
return nil, 0, err
}
return s, length, nil
}
// Open returns an io.ReadCloser that can be used to read the stream's contents.
func (s *Stream) Open() (io.ReadCloser, error) {
return s.wim.resourceReader(&s.offset)
}
// Open returns an io.ReadCloser that can be used to read the file's contents.
func (f *File) Open() (io.ReadCloser, error) {
return f.img.wim.resourceReader(&f.offset)
}
// Readdir reads the directory entries.
func (f *File) Readdir() ([]*File, error) {
if !f.IsDir() {
return nil, errors.New("not a directory")
}
return f.img.readdir(f.subdirOffset)
}
// IsDir returns whether the given file is a directory. It returns false when it
// is a directory reparse point.
func (f *FileHeader) IsDir() bool {
return f.Attributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_DIRECTORY
}