867 lines
21 KiB
Go
867 lines
21 KiB
Go
|
// 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"
|
||
|
"encoding/binary"
|
||
|
"encoding/xml"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"strconv"
|
||
|
"sync"
|
||
|
"time"
|
||
|
"unicode/utf16"
|
||
|
)
|
||
|
|
||
|
// File attribute constants from Windows.
|
||
|
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.
|
||
|
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}
|
||
|
|
||
|
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
|
||
|
|
||
|
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
|
||
|
|
||
|
const (
|
||
|
hdrFlagReserved hdrFlag = 1 << iota
|
||
|
hdrFlagCompressed
|
||
|
hdrFlagReadOnly
|
||
|
hdrFlagSpanned
|
||
|
hdrFlagResourceOnly
|
||
|
hdrFlagMetadataOnly
|
||
|
hdrFlagWriteInProgress
|
||
|
hdrFlagRpFix
|
||
|
)
|
||
|
|
||
|
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 unmarshals 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())
|
||
|
}
|
||
|
|
||
|
// 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 info info
|
||
|
err = xml.Unmarshal([]byte(xmlinfo), &info)
|
||
|
if err != nil {
|
||
|
return nil, &ParseError{Oper: "XML info", Err: err}
|
||
|
}
|
||
|
|
||
|
for i, img := range images {
|
||
|
for _, imgInfo := range info.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 = ioutil.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 ioutil.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 {
|
||
|
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()
|
||
|
_, 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 (r *Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, err error) {
|
||
|
var secBlock securityblockDisk
|
||
|
err = binary.Read(rsrc, binary.LittleEndian, &secBlock)
|
||
|
if err != nil {
|
||
|
err = &ParseError{Oper: "security table", Err: err}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
n += securityblockDiskSize
|
||
|
|
||
|
secSizes := make([]int64, secBlock.NumEntries)
|
||
|
err = binary.Read(rsrc, binary.LittleEndian, &secSizes)
|
||
|
if err != nil {
|
||
|
err = &ParseError{Oper: "security table sizes", Err: err}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
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 {
|
||
|
err = &ParseError{Oper: "security descriptor", Err: err}
|
||
|
return
|
||
|
}
|
||
|
n += int64(len(sd))
|
||
|
sds[i] = sd
|
||
|
}
|
||
|
|
||
|
secsize := int64((secBlock.TotalLength + 7) &^ 7)
|
||
|
if n > secsize {
|
||
|
err = &ParseError{Oper: "security descriptor", Err: errors.New("security descriptor table too small")}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, err = io.CopyN(ioutil.Discard, rsrc, secsize-n)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
n = secsize
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// 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(ioutil.Discard, img.r, offset-img.curOffset)
|
||
|
if err != nil {
|
||
|
img.reset()
|
||
|
if err == io.EOF {
|
||
|
err = io.ErrUnexpectedEOF
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var entries []*File
|
||
|
for {
|
||
|
e, n, err := img.readNextEntry(img.r)
|
||
|
img.curOffset += n
|
||
|
if err == io.EOF {
|
||
|
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(ioutil.Discard, r, left)
|
||
|
if err != nil {
|
||
|
if err == io.EOF {
|
||
|
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 {
|
||
|
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(ioutil.Discard, r, left)
|
||
|
if err != nil {
|
||
|
if err == io.EOF {
|
||
|
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
|
||
|
}
|