From 3cedae2e26c9739f98b9ca713db6ef3797f6dbf1 Mon Sep 17 00:00:00 2001 From: John Starks Date: Wed, 6 Apr 2016 13:56:25 -0700 Subject: [PATCH] WIM: Fix handling of reparse points Reparse point data is not always in the first stream, sometimes it is in the file data itself. Remove ReparseStream and always put it in the file data. --- wim/wim.go | 163 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 94 insertions(+), 69 deletions(-) diff --git a/wim/wim.go b/wim/wim.go index ffd2ee5..dca93ab 100644 --- a/wim/wim.go +++ b/wim/wim.go @@ -153,11 +153,15 @@ const streamentrySize = 38 // ParseError is returned when the WIM cannot be parsed. type ParseError struct { Oper string + Path string Err error } func (e *ParseError) Error() string { - return "WIM parse error at " + e.Oper + ": " + e.Err.Error() + 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. @@ -205,7 +209,6 @@ type FileHeader struct { LinkID int64 ReparseTag uint32 ReparseReserved uint32 - ReparseStream *Stream } // File represents a file or directory in a WIM image. @@ -227,7 +230,7 @@ func NewReader(f io.ReaderAt) (*Reader, error) { } if r.hdr.ImageTag != wimImageTag { - return nil, &ParseError{"image tag", errors.New("not a WIM file")} + return nil, &ParseError{Oper: "image tag", Err: errors.New("not a WIM file")} } if r.hdr.Flags&^supportedHdrFlags != 0 { @@ -295,12 +298,12 @@ func (r *Reader) ReadXML() (string, error) { XMLData := make([]uint16, r.hdr.XMLData.OriginalSize/2) err = binary.Read(rsrc, binary.LittleEndian, XMLData) if err != nil { - return "", &ParseError{"XML data", err} + return "", &ParseError{Oper: "XML data", Err: err} } // The BOM will always indicate little-endian UTF-16. if XMLData[0] != 0xfeff { - return "", &ParseError{"XML data", errors.New("invalid BOM")} + return "", &ParseError{Oper: "XML data", Err: errors.New("invalid BOM")} } return string(utf16.Decode(XMLData[1:])), nil } @@ -311,39 +314,39 @@ func (r *Reader) readOffsetTable(res *resourceDescriptor) (map[SHA1Hash]resource offsetTable, err := r.readResource(res) if err != nil { - return nil, nil, &ParseError{"offset table", err} + return nil, nil, &ParseError{Oper: "offset table", Err: err} } br := bytes.NewReader(offsetTable) - for { + 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{"offset table", err} + return nil, nil, &ParseError{Oper: "offset table", Err: err} } if res.Flags()&^supportedResFlags != 0 { - return nil, nil, &ParseError{"offset table", errors.New("unsupported resource flag")} + 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 { - return nil, nil, err + panic(fmt.Sprint(i, err)) } hash := sha1.New() _, err = io.Copy(hash, sec) sec.Close() if err != nil { - return nil, nil, err + panic(fmt.Sprint(i, err)) } var cmphash SHA1Hash copy(cmphash[:], hash.Sum(nil)) if cmphash != res.Hash { - return nil, nil, errors.New("hash mismatch") + panic(fmt.Sprint(i, "hash mismatch")) } } @@ -359,7 +362,7 @@ func (r *Reader) readOffsetTable(res *resourceDescriptor) (map[SHA1Hash]resource } if len(images) != int(r.hdr.ImageCount) { - return nil, nil, &ParseError{"offset table", errors.New("mismatched image count")} + return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("mismatched image count")} } return fileData, images, nil @@ -369,7 +372,7 @@ func (r *Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, var secBlock securityblockDisk err = binary.Read(rsrc, binary.LittleEndian, &secBlock) if err != nil { - err = &ParseError{"security table", err} + err = &ParseError{Oper: "security table", Err: err} return } @@ -378,7 +381,7 @@ func (r *Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, secSizes := make([]int64, secBlock.NumEntries) err = binary.Read(rsrc, binary.LittleEndian, &secSizes) if err != nil { - err = &ParseError{"security table sizes", err} + err = &ParseError{Oper: "security table sizes", Err: err} return } @@ -389,7 +392,7 @@ func (r *Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, sd := make([]byte, size&0xffffffff) _, err = io.ReadFull(rsrc, sd) if err != nil { - err = &ParseError{"security descriptor", err} + err = &ParseError{Oper: "security descriptor", Err: err} return } n += int64(len(sd)) @@ -398,7 +401,7 @@ func (r *Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, secsize := int64((secBlock.TotalLength + 7) &^ 7) if n > secsize { - err = &ParseError{"security descriptor", errors.New("security descriptor table too small")} + err = &ParseError{Oper: "security descriptor", Err: errors.New("security descriptor table too small")} return } @@ -432,7 +435,7 @@ func (img *Image) Open() (*File, error) { return nil, err } if len(f) != 1 { - return nil, &ParseError{"root directory", errors.New("expected exactly 1 root directory entry")} + return nil, &ParseError{Oper: "root directory", Err: errors.New("expected exactly 1 root directory entry")} } return f[0], err } @@ -457,7 +460,7 @@ func (img *Image) readdir(rsrc io.Reader) ([]*File, error) { func (img *Image) readNextEntry(r *bufio.Reader) (*File, error) { lengthBuf, err := r.Peek(8) if err != nil { - return nil, &ParseError{"directory length check", err} + return nil, &ParseError{Oper: "directory length check", Err: err} } left := int(binary.LittleEndian.Uint64(lengthBuf)) @@ -466,24 +469,46 @@ func (img *Image) readNextEntry(r *bufio.Reader) (*File, error) { } if left < direntrySize { - return nil, &ParseError{"directory entry", errors.New("size too short")} + return nil, &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, &ParseError{"directory entry", err} + return nil, &ParseError{Oper: "directory entry", Err: err} } left -= direntrySize + namesLen := int(dentry.FileNameLength + 2 + dentry.ShortNameLength) + if left < namesLen { + return nil, &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, &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, &ParseError{"directory entry", fmt.Errorf("could not find file data matching hash %v", dentry.Hash)} + return nil, &ParseError{Oper: "directory entry", Path: name, Err: fmt.Errorf("could not find file data matching hash %#v", dentry)} } } @@ -495,6 +520,8 @@ func (img *Image) readNextEntry(r *bufio.Reader) (*File, error) { LastWriteTime: dentry.LastWriteTime, Hash: dentry.Hash, Size: offset.OriginalSize, + Name: name, + ShortName: shortName, }, offset: offset, @@ -502,38 +529,28 @@ func (img *Image) readNextEntry(r *bufio.Reader) (*File, error) { subdirOffset: dentry.SubdirOffset, } + isDir := false + if dentry.Attributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT == 0 { f.LinkID = dentry.ReparseHardLink + if dentry.Attributes&syscall.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, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("no subdirectory data for directory")} + } else if !isDir && f.subdirOffset != 0 { + return nil, &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] } - namesLen := int(dentry.FileNameLength + 2 + dentry.ShortNameLength) - if left < namesLen { - return nil, &ParseError{"directory entry", errors.New("size too short for names")} - } - - names := make([]uint16, namesLen/2) - err = binary.Read(r, binary.LittleEndian, names) - if err != nil { - return nil, &ParseError{"file name", err} - } - - left -= namesLen - - if dentry.FileNameLength > 0 { - f.Name = string(utf16.Decode(names[:dentry.FileNameLength/2])) - } - - if dentry.ShortNameLength > 0 { - f.ShortName = string(utf16.Decode(names[dentry.FileNameLength/2+1:])) - } - _, err = r.Discard(left) if err != nil { return nil, err @@ -546,19 +563,20 @@ func (img *Image) readNextEntry(r *bufio.Reader) (*File, error) { if err != nil { return nil, err } - if !(s.Name == "" && s.Size == 0) { - if dentry.Attributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && s.Name == "" { - f.ReparseStream = s - } else { - streams = append(streams, s) - } + // 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&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && f.ReparseStream == nil { - return nil, &ParseError{"directory entry", errors.New("reparse point is missing reparse stream")} + if dentry.Attributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 && f.Size == 0 { + return nil, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("reparse point is missing reparse stream")} } return f, nil @@ -567,28 +585,41 @@ func (img *Image) readNextEntry(r *bufio.Reader) (*File, error) { func (img *Image) readNextStream(r *bufio.Reader) (*Stream, error) { lengthBuf, err := r.Peek(8) if err != nil { - return nil, &ParseError{"stream length check", err} + return nil, &ParseError{Oper: "stream length check", Err: err} } left := int(binary.LittleEndian.Uint64(lengthBuf)) if left < streamentrySize { - return nil, &ParseError{"stream entry", errors.New("size too short")} + return nil, &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, &ParseError{"stream entry", err} + return nil, &ParseError{Oper: "stream entry", Err: err} } left -= streamentrySize + if left < int(sentry.NameLength) { + return nil, &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, &ParseError{Oper: "file name", Err: err} + } + + left -= int(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, &ParseError{"stream entry", fmt.Errorf("could not find file data matching hash %v", sentry.Hash)} + return nil, &ParseError{Oper: "stream entry", Path: name, Err: fmt.Errorf("could not find file data matching hash %v", sentry.Hash)} } } @@ -596,24 +627,12 @@ func (img *Image) readNextStream(r *bufio.Reader) (*Stream, error) { StreamHeader: StreamHeader{ Hash: sentry.Hash, Size: offset.OriginalSize, + Name: name, }, wim: img.wim, offset: offset, } - if left < int(sentry.NameLength) { - return nil, &ParseError{"stream entry", 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, &ParseError{"file name", err} - } - - left -= int(sentry.NameLength) - s.Name = string(utf16.Decode(names)) - _, err = r.Discard(left) if err != nil { return nil, err @@ -634,7 +653,7 @@ func (f *File) Open() (io.ReadCloser, error) { // Readdir reads the directory entries. func (f *File) Readdir() ([]*File, error) { - if f.Attributes&syscall.FILE_ATTRIBUTE_DIRECTORY == 0 { + if !f.IsDir() { return nil, errors.New("not a directory") } rsrc, err := f.img.wim.resourceReaderWithOffset(&f.img.offset, f.subdirOffset) @@ -644,3 +663,9 @@ func (f *File) Readdir() ([]*File, error) { defer rsrc.Close() return f.img.readdir(rsrc) } + +// 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&(syscall.FILE_ATTRIBUTE_DIRECTORY|syscall.FILE_ATTRIBUTE_REPARSE_POINT) == syscall.FILE_ATTRIBUTE_DIRECTORY +}