1// Package wim implements a WIM file parser.
2//
3// WIM files are used to distribute Windows file system and container images.
4// They are documented at https://msdn.microsoft.com/en-us/library/windows/desktop/dd861280.aspx.
5package wim
6
7import (
8	"bytes"
9	"crypto/sha1"
10	"encoding/binary"
11	"encoding/xml"
12	"errors"
13	"fmt"
14	"io"
15	"io/ioutil"
16	"strconv"
17	"sync"
18	"time"
19	"unicode/utf16"
20)
21
22// File attribute constants from Windows.
23const (
24	FILE_ATTRIBUTE_READONLY            = 0x00000001
25	FILE_ATTRIBUTE_HIDDEN              = 0x00000002
26	FILE_ATTRIBUTE_SYSTEM              = 0x00000004
27	FILE_ATTRIBUTE_DIRECTORY           = 0x00000010
28	FILE_ATTRIBUTE_ARCHIVE             = 0x00000020
29	FILE_ATTRIBUTE_DEVICE              = 0x00000040
30	FILE_ATTRIBUTE_NORMAL              = 0x00000080
31	FILE_ATTRIBUTE_TEMPORARY           = 0x00000100
32	FILE_ATTRIBUTE_SPARSE_FILE         = 0x00000200
33	FILE_ATTRIBUTE_REPARSE_POINT       = 0x00000400
34	FILE_ATTRIBUTE_COMPRESSED          = 0x00000800
35	FILE_ATTRIBUTE_OFFLINE             = 0x00001000
36	FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000
37	FILE_ATTRIBUTE_ENCRYPTED           = 0x00004000
38	FILE_ATTRIBUTE_INTEGRITY_STREAM    = 0x00008000
39	FILE_ATTRIBUTE_VIRTUAL             = 0x00010000
40	FILE_ATTRIBUTE_NO_SCRUB_DATA       = 0x00020000
41	FILE_ATTRIBUTE_EA                  = 0x00040000
42)
43
44// Windows processor architectures.
45const (
46	PROCESSOR_ARCHITECTURE_INTEL         = 0
47	PROCESSOR_ARCHITECTURE_MIPS          = 1
48	PROCESSOR_ARCHITECTURE_ALPHA         = 2
49	PROCESSOR_ARCHITECTURE_PPC           = 3
50	PROCESSOR_ARCHITECTURE_SHX           = 4
51	PROCESSOR_ARCHITECTURE_ARM           = 5
52	PROCESSOR_ARCHITECTURE_IA64          = 6
53	PROCESSOR_ARCHITECTURE_ALPHA64       = 7
54	PROCESSOR_ARCHITECTURE_MSIL          = 8
55	PROCESSOR_ARCHITECTURE_AMD64         = 9
56	PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 = 10
57	PROCESSOR_ARCHITECTURE_NEUTRAL       = 11
58	PROCESSOR_ARCHITECTURE_ARM64         = 12
59)
60
61var wimImageTag = [...]byte{'M', 'S', 'W', 'I', 'M', 0, 0, 0}
62
63type guid struct {
64	Data1 uint32
65	Data2 uint16
66	Data3 uint16
67	Data4 [8]byte
68}
69
70func (g guid) String() string {
71	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])
72}
73
74type resourceDescriptor struct {
75	FlagsAndCompressedSize uint64
76	Offset                 int64
77	OriginalSize           int64
78}
79
80type resFlag byte
81
82const (
83	resFlagFree resFlag = 1 << iota
84	resFlagMetadata
85	resFlagCompressed
86	resFlagSpanned
87)
88
89const validate = false
90
91const supportedResFlags = resFlagMetadata | resFlagCompressed
92
93func (r *resourceDescriptor) Flags() resFlag {
94	return resFlag(r.FlagsAndCompressedSize >> 56)
95}
96
97func (r *resourceDescriptor) CompressedSize() int64 {
98	return int64(r.FlagsAndCompressedSize & 0xffffffffffffff)
99}
100
101func (r *resourceDescriptor) String() string {
102	s := fmt.Sprintf("%d bytes at %d", r.CompressedSize(), r.Offset)
103	if r.Flags()&4 != 0 {
104		s += fmt.Sprintf(" (uncompresses to %d)", r.OriginalSize)
105	}
106	return s
107}
108
109// SHA1Hash contains the SHA1 hash of a file or stream.
110type SHA1Hash [20]byte
111
112type streamDescriptor struct {
113	resourceDescriptor
114	PartNumber uint16
115	RefCount   uint32
116	Hash       SHA1Hash
117}
118
119type hdrFlag uint32
120
121const (
122	hdrFlagReserved hdrFlag = 1 << iota
123	hdrFlagCompressed
124	hdrFlagReadOnly
125	hdrFlagSpanned
126	hdrFlagResourceOnly
127	hdrFlagMetadataOnly
128	hdrFlagWriteInProgress
129	hdrFlagRpFix
130)
131
132const (
133	hdrFlagCompressReserved hdrFlag = 1 << (iota + 16)
134	hdrFlagCompressXpress
135	hdrFlagCompressLzx
136)
137
138const supportedHdrFlags = hdrFlagRpFix | hdrFlagReadOnly | hdrFlagCompressed | hdrFlagCompressLzx
139
140type wimHeader struct {
141	ImageTag        [8]byte
142	Size            uint32
143	Version         uint32
144	Flags           hdrFlag
145	CompressionSize uint32
146	WIMGuid         guid
147	PartNumber      uint16
148	TotalParts      uint16
149	ImageCount      uint32
150	OffsetTable     resourceDescriptor
151	XMLData         resourceDescriptor
152	BootMetadata    resourceDescriptor
153	BootIndex       uint32
154	Padding         uint32
155	Integrity       resourceDescriptor
156	Unused          [60]byte
157}
158
159type securityblockDisk struct {
160	TotalLength uint32
161	NumEntries  uint32
162}
163
164const securityblockDiskSize = 8
165
166type direntry struct {
167	Attributes       uint32
168	SecurityID       uint32
169	SubdirOffset     int64
170	Unused1, Unused2 int64
171	CreationTime     Filetime
172	LastAccessTime   Filetime
173	LastWriteTime    Filetime
174	Hash             SHA1Hash
175	Padding          uint32
176	ReparseHardLink  int64
177	StreamCount      uint16
178	ShortNameLength  uint16
179	FileNameLength   uint16
180}
181
182var direntrySize = int64(binary.Size(direntry{}) + 8) // includes an 8-byte length prefix
183
184type streamentry struct {
185	Unused     int64
186	Hash       SHA1Hash
187	NameLength int16
188}
189
190var streamentrySize = int64(binary.Size(streamentry{}) + 8) // includes an 8-byte length prefix
191
192// Filetime represents a Windows time.
193type Filetime struct {
194	LowDateTime  uint32
195	HighDateTime uint32
196}
197
198// Time returns the time as time.Time.
199func (ft *Filetime) Time() time.Time {
200	// 100-nanosecond intervals since January 1, 1601
201	nsec := int64(ft.HighDateTime)<<32 + int64(ft.LowDateTime)
202	// change starting time to the Epoch (00:00:00 UTC, January 1, 1970)
203	nsec -= 116444736000000000
204	// convert into nanoseconds
205	nsec *= 100
206	return time.Unix(0, nsec)
207}
208
209// UnmarshalXML unmarshals the time from a WIM XML blob.
210func (ft *Filetime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
211	type time struct {
212		Low  string `xml:"LOWPART"`
213		High string `xml:"HIGHPART"`
214	}
215	var t time
216	err := d.DecodeElement(&t, &start)
217	if err != nil {
218		return err
219	}
220
221	low, err := strconv.ParseUint(t.Low, 0, 32)
222	if err != nil {
223		return err
224	}
225	high, err := strconv.ParseUint(t.High, 0, 32)
226	if err != nil {
227		return err
228	}
229
230	ft.LowDateTime = uint32(low)
231	ft.HighDateTime = uint32(high)
232	return nil
233}
234
235type info struct {
236	Image []ImageInfo `xml:"IMAGE"`
237}
238
239// ImageInfo contains information about the image.
240type ImageInfo struct {
241	Name         string       `xml:"NAME"`
242	Index        int          `xml:"INDEX,attr"`
243	CreationTime Filetime     `xml:"CREATIONTIME"`
244	ModTime      Filetime     `xml:"LASTMODIFICATIONTIME"`
245	Windows      *WindowsInfo `xml:"WINDOWS"`
246}
247
248// WindowsInfo contains information about the Windows installation in the image.
249type WindowsInfo struct {
250	Arch             byte     `xml:"ARCH"`
251	ProductName      string   `xml:"PRODUCTNAME"`
252	EditionID        string   `xml:"EDITIONID"`
253	InstallationType string   `xml:"INSTALLATIONTYPE"`
254	ProductType      string   `xml:"PRODUCTTYPE"`
255	Languages        []string `xml:"LANGUAGES>LANGUAGE"`
256	DefaultLanguage  string   `xml:"LANGUAGES>DEFAULT"`
257	Version          Version  `xml:"VERSION"`
258	SystemRoot       string   `xml:"SYSTEMROOT"`
259}
260
261// Version represents a Windows build version.
262type Version struct {
263	Major   int `xml:"MAJOR"`
264	Minor   int `xml:"MINOR"`
265	Build   int `xml:"BUILD"`
266	SPBuild int `xml:"SPBUILD"`
267	SPLevel int `xml:"SPLEVEL"`
268}
269
270// ParseError is returned when the WIM cannot be parsed.
271type ParseError struct {
272	Oper string
273	Path string
274	Err  error
275}
276
277func (e *ParseError) Error() string {
278	if e.Path == "" {
279		return "WIM parse error at " + e.Oper + ": " + e.Err.Error()
280	}
281	return fmt.Sprintf("WIM parse error: %s %s: %s", e.Oper, e.Path, e.Err.Error())
282}
283
284// Reader provides functions to read a WIM file.
285type Reader struct {
286	hdr      wimHeader
287	r        io.ReaderAt
288	fileData map[SHA1Hash]resourceDescriptor
289
290	XMLInfo string   // The XML information about the WIM.
291	Image   []*Image // The WIM's images.
292}
293
294// Image represents an image within a WIM file.
295type Image struct {
296	wim        *Reader
297	offset     resourceDescriptor
298	sds        [][]byte
299	rootOffset int64
300	r          io.ReadCloser
301	curOffset  int64
302	m          sync.Mutex
303
304	ImageInfo
305}
306
307// StreamHeader contains alternate data stream metadata.
308type StreamHeader struct {
309	Name string
310	Hash SHA1Hash
311	Size int64
312}
313
314// Stream represents an alternate data stream or reparse point data stream.
315type Stream struct {
316	StreamHeader
317	wim    *Reader
318	offset resourceDescriptor
319}
320
321// FileHeader contains file metadata.
322type FileHeader struct {
323	Name               string
324	ShortName          string
325	Attributes         uint32
326	SecurityDescriptor []byte
327	CreationTime       Filetime
328	LastAccessTime     Filetime
329	LastWriteTime      Filetime
330	Hash               SHA1Hash
331	Size               int64
332	LinkID             int64
333	ReparseTag         uint32
334	ReparseReserved    uint32
335}
336
337// File represents a file or directory in a WIM image.
338type File struct {
339	FileHeader
340	Streams      []*Stream
341	offset       resourceDescriptor
342	img          *Image
343	subdirOffset int64
344}
345
346// NewReader returns a Reader that can be used to read WIM file data.
347func NewReader(f io.ReaderAt) (*Reader, error) {
348	r := &Reader{r: f}
349	section := io.NewSectionReader(f, 0, 0xffff)
350	err := binary.Read(section, binary.LittleEndian, &r.hdr)
351	if err != nil {
352		return nil, err
353	}
354
355	if r.hdr.ImageTag != wimImageTag {
356		return nil, &ParseError{Oper: "image tag", Err: errors.New("not a WIM file")}
357	}
358
359	if r.hdr.Flags&^supportedHdrFlags != 0 {
360		return nil, fmt.Errorf("unsupported WIM flags %x", r.hdr.Flags&^supportedHdrFlags)
361	}
362
363	if r.hdr.CompressionSize != 0x8000 {
364		return nil, fmt.Errorf("unsupported compression size %d", r.hdr.CompressionSize)
365	}
366
367	if r.hdr.TotalParts != 1 {
368		return nil, errors.New("multi-part WIM not supported")
369	}
370
371	fileData, images, err := r.readOffsetTable(&r.hdr.OffsetTable)
372	if err != nil {
373		return nil, err
374	}
375
376	xmlinfo, err := r.readXML()
377	if err != nil {
378		return nil, err
379	}
380
381	var info info
382	err = xml.Unmarshal([]byte(xmlinfo), &info)
383	if err != nil {
384		return nil, &ParseError{Oper: "XML info", Err: err}
385	}
386
387	for i, img := range images {
388		for _, imgInfo := range info.Image {
389			if imgInfo.Index == i+1 {
390				img.ImageInfo = imgInfo
391				break
392			}
393		}
394	}
395
396	r.fileData = fileData
397	r.Image = images
398	r.XMLInfo = xmlinfo
399	return r, nil
400}
401
402// Close releases resources associated with the Reader.
403func (r *Reader) Close() error {
404	for _, img := range r.Image {
405		img.reset()
406	}
407	return nil
408}
409
410func (r *Reader) resourceReader(hdr *resourceDescriptor) (io.ReadCloser, error) {
411	return r.resourceReaderWithOffset(hdr, 0)
412}
413
414func (r *Reader) resourceReaderWithOffset(hdr *resourceDescriptor, offset int64) (io.ReadCloser, error) {
415	var sr io.ReadCloser
416	section := io.NewSectionReader(r.r, hdr.Offset, hdr.CompressedSize())
417	if hdr.Flags()&resFlagCompressed == 0 {
418		section.Seek(offset, 0)
419		sr = ioutil.NopCloser(section)
420	} else {
421		cr, err := newCompressedReader(section, hdr.OriginalSize, offset)
422		if err != nil {
423			return nil, err
424		}
425		sr = cr
426	}
427
428	return sr, nil
429}
430
431func (r *Reader) readResource(hdr *resourceDescriptor) ([]byte, error) {
432	rsrc, err := r.resourceReader(hdr)
433	if err != nil {
434		return nil, err
435	}
436	defer rsrc.Close()
437	return ioutil.ReadAll(rsrc)
438}
439
440func (r *Reader) readXML() (string, error) {
441	if r.hdr.XMLData.CompressedSize() == 0 {
442		return "", nil
443	}
444	rsrc, err := r.resourceReader(&r.hdr.XMLData)
445	if err != nil {
446		return "", err
447	}
448	defer rsrc.Close()
449
450	XMLData := make([]uint16, r.hdr.XMLData.OriginalSize/2)
451	err = binary.Read(rsrc, binary.LittleEndian, XMLData)
452	if err != nil {
453		return "", &ParseError{Oper: "XML data", Err: err}
454	}
455
456	// The BOM will always indicate little-endian UTF-16.
457	if XMLData[0] != 0xfeff {
458		return "", &ParseError{Oper: "XML data", Err: errors.New("invalid BOM")}
459	}
460	return string(utf16.Decode(XMLData[1:])), nil
461}
462
463func (r *Reader) readOffsetTable(res *resourceDescriptor) (map[SHA1Hash]resourceDescriptor, []*Image, error) {
464	fileData := make(map[SHA1Hash]resourceDescriptor)
465	var images []*Image
466
467	offsetTable, err := r.readResource(res)
468	if err != nil {
469		return nil, nil, &ParseError{Oper: "offset table", Err: err}
470	}
471
472	br := bytes.NewReader(offsetTable)
473	for i := 0; ; i++ {
474		var res streamDescriptor
475		err := binary.Read(br, binary.LittleEndian, &res)
476		if err == io.EOF {
477			break
478		}
479		if err != nil {
480			return nil, nil, &ParseError{Oper: "offset table", Err: err}
481		}
482		if res.Flags()&^supportedResFlags != 0 {
483			return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("unsupported resource flag")}
484		}
485
486		// Validation for ad-hoc testing
487		if validate {
488			sec, err := r.resourceReader(&res.resourceDescriptor)
489			if err != nil {
490				panic(fmt.Sprint(i, err))
491			}
492			hash := sha1.New()
493			_, err = io.Copy(hash, sec)
494			sec.Close()
495			if err != nil {
496				panic(fmt.Sprint(i, err))
497			}
498			var cmphash SHA1Hash
499			copy(cmphash[:], hash.Sum(nil))
500			if cmphash != res.Hash {
501				panic(fmt.Sprint(i, "hash mismatch"))
502			}
503		}
504
505		if res.Flags()&resFlagMetadata != 0 {
506			image := &Image{
507				wim:    r,
508				offset: res.resourceDescriptor,
509			}
510			images = append(images, image)
511		} else {
512			fileData[res.Hash] = res.resourceDescriptor
513		}
514	}
515
516	if len(images) != int(r.hdr.ImageCount) {
517		return nil, nil, &ParseError{Oper: "offset table", Err: errors.New("mismatched image count")}
518	}
519
520	return fileData, images, nil
521}
522
523func (r *Reader) readSecurityDescriptors(rsrc io.Reader) (sds [][]byte, n int64, err error) {
524	var secBlock securityblockDisk
525	err = binary.Read(rsrc, binary.LittleEndian, &secBlock)
526	if err != nil {
527		err = &ParseError{Oper: "security table", Err: err}
528		return
529	}
530
531	n += securityblockDiskSize
532
533	secSizes := make([]int64, secBlock.NumEntries)
534	err = binary.Read(rsrc, binary.LittleEndian, &secSizes)
535	if err != nil {
536		err = &ParseError{Oper: "security table sizes", Err: err}
537		return
538	}
539
540	n += int64(secBlock.NumEntries * 8)
541
542	sds = make([][]byte, secBlock.NumEntries)
543	for i, size := range secSizes {
544		sd := make([]byte, size&0xffffffff)
545		_, err = io.ReadFull(rsrc, sd)
546		if err != nil {
547			err = &ParseError{Oper: "security descriptor", Err: err}
548			return
549		}
550		n += int64(len(sd))
551		sds[i] = sd
552	}
553
554	secsize := int64((secBlock.TotalLength + 7) &^ 7)
555	if n > secsize {
556		err = &ParseError{Oper: "security descriptor", Err: errors.New("security descriptor table too small")}
557		return
558	}
559
560	_, err = io.CopyN(ioutil.Discard, rsrc, secsize-n)
561	if err != nil {
562		return
563	}
564
565	n = secsize
566	return
567}
568
569// Open parses the image and returns the root directory.
570func (img *Image) Open() (*File, error) {
571	if img.sds == nil {
572		rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, img.rootOffset)
573		if err != nil {
574			return nil, err
575		}
576		sds, n, err := img.wim.readSecurityDescriptors(rsrc)
577		if err != nil {
578			rsrc.Close()
579			return nil, err
580		}
581		img.sds = sds
582		img.r = rsrc
583		img.rootOffset = n
584		img.curOffset = n
585	}
586
587	f, err := img.readdir(img.rootOffset)
588	if err != nil {
589		return nil, err
590	}
591	if len(f) != 1 {
592		return nil, &ParseError{Oper: "root directory", Err: errors.New("expected exactly 1 root directory entry")}
593	}
594	return f[0], err
595}
596
597func (img *Image) reset() {
598	if img.r != nil {
599		img.r.Close()
600		img.r = nil
601	}
602	img.curOffset = -1
603}
604
605func (img *Image) readdir(offset int64) ([]*File, error) {
606	img.m.Lock()
607	defer img.m.Unlock()
608
609	if offset < img.curOffset || offset > img.curOffset+chunkSize {
610		// Reset to seek backward or to seek forward very far.
611		img.reset()
612	}
613	if img.r == nil {
614		rsrc, err := img.wim.resourceReaderWithOffset(&img.offset, offset)
615		if err != nil {
616			return nil, err
617		}
618		img.r = rsrc
619		img.curOffset = offset
620	}
621	if offset > img.curOffset {
622		_, err := io.CopyN(ioutil.Discard, img.r, offset-img.curOffset)
623		if err != nil {
624			img.reset()
625			if err == io.EOF {
626				err = io.ErrUnexpectedEOF
627			}
628			return nil, err
629		}
630	}
631
632	var entries []*File
633	for {
634		e, n, err := img.readNextEntry(img.r)
635		img.curOffset += n
636		if err == io.EOF {
637			break
638		}
639		if err != nil {
640			img.reset()
641			return nil, err
642		}
643		entries = append(entries, e)
644	}
645	return entries, nil
646}
647
648func (img *Image) readNextEntry(r io.Reader) (*File, int64, error) {
649	var length int64
650	err := binary.Read(r, binary.LittleEndian, &length)
651	if err != nil {
652		return nil, 0, &ParseError{Oper: "directory length check", Err: err}
653	}
654
655	if length == 0 {
656		return nil, 8, io.EOF
657	}
658
659	left := length
660	if left < direntrySize {
661		return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short")}
662	}
663
664	var dentry direntry
665	err = binary.Read(r, binary.LittleEndian, &dentry)
666	if err != nil {
667		return nil, 0, &ParseError{Oper: "directory entry", Err: err}
668	}
669
670	left -= direntrySize
671
672	namesLen := int64(dentry.FileNameLength + 2 + dentry.ShortNameLength)
673	if left < namesLen {
674		return nil, 0, &ParseError{Oper: "directory entry", Err: errors.New("size too short for names")}
675	}
676
677	names := make([]uint16, namesLen/2)
678	err = binary.Read(r, binary.LittleEndian, names)
679	if err != nil {
680		return nil, 0, &ParseError{Oper: "file name", Err: err}
681	}
682
683	left -= namesLen
684
685	var name, shortName string
686	if dentry.FileNameLength > 0 {
687		name = string(utf16.Decode(names[:dentry.FileNameLength/2]))
688	}
689
690	if dentry.ShortNameLength > 0 {
691		shortName = string(utf16.Decode(names[dentry.FileNameLength/2+1:]))
692	}
693
694	var offset resourceDescriptor
695	zerohash := SHA1Hash{}
696	if dentry.Hash != zerohash {
697		var ok bool
698		offset, ok = img.wim.fileData[dentry.Hash]
699		if !ok {
700			return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: fmt.Errorf("could not find file data matching hash %#v", dentry)}
701		}
702	}
703
704	f := &File{
705		FileHeader: FileHeader{
706			Attributes:     dentry.Attributes,
707			CreationTime:   dentry.CreationTime,
708			LastAccessTime: dentry.LastAccessTime,
709			LastWriteTime:  dentry.LastWriteTime,
710			Hash:           dentry.Hash,
711			Size:           offset.OriginalSize,
712			Name:           name,
713			ShortName:      shortName,
714		},
715
716		offset:       offset,
717		img:          img,
718		subdirOffset: dentry.SubdirOffset,
719	}
720
721	isDir := false
722
723	if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT == 0 {
724		f.LinkID = dentry.ReparseHardLink
725		if dentry.Attributes&FILE_ATTRIBUTE_DIRECTORY != 0 {
726			isDir = true
727		}
728	} else {
729		f.ReparseTag = uint32(dentry.ReparseHardLink)
730		f.ReparseReserved = uint32(dentry.ReparseHardLink >> 32)
731	}
732
733	if isDir && f.subdirOffset == 0 {
734		return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("no subdirectory data for directory")}
735	} else if !isDir && f.subdirOffset != 0 {
736		return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("unexpected subdirectory data for non-directory")}
737	}
738
739	if dentry.SecurityID != 0xffffffff {
740		f.SecurityDescriptor = img.sds[dentry.SecurityID]
741	}
742
743	_, err = io.CopyN(ioutil.Discard, r, left)
744	if err != nil {
745		if err == io.EOF {
746			err = io.ErrUnexpectedEOF
747		}
748		return nil, 0, err
749	}
750
751	if dentry.StreamCount > 0 {
752		var streams []*Stream
753		for i := uint16(0); i < dentry.StreamCount; i++ {
754			s, n, err := img.readNextStream(r)
755			length += n
756			if err != nil {
757				return nil, 0, err
758			}
759			// The first unnamed stream should be treated as the file stream.
760			if i == 0 && s.Name == "" {
761				f.Hash = s.Hash
762				f.Size = s.Size
763				f.offset = s.offset
764			} else if s.Name != "" {
765				streams = append(streams, s)
766			}
767		}
768		f.Streams = streams
769	}
770
771	if dentry.Attributes&FILE_ATTRIBUTE_REPARSE_POINT != 0 && f.Size == 0 {
772		return nil, 0, &ParseError{Oper: "directory entry", Path: name, Err: errors.New("reparse point is missing reparse stream")}
773	}
774
775	return f, length, nil
776}
777
778func (img *Image) readNextStream(r io.Reader) (*Stream, int64, error) {
779	var length int64
780	err := binary.Read(r, binary.LittleEndian, &length)
781	if err != nil {
782		if err == io.EOF {
783			err = io.ErrUnexpectedEOF
784		}
785		return nil, 0, &ParseError{Oper: "stream length check", Err: err}
786	}
787
788	left := length
789	if left < streamentrySize {
790		return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short")}
791	}
792
793	var sentry streamentry
794	err = binary.Read(r, binary.LittleEndian, &sentry)
795	if err != nil {
796		return nil, 0, &ParseError{Oper: "stream entry", Err: err}
797	}
798
799	left -= streamentrySize
800
801	if left < int64(sentry.NameLength) {
802		return nil, 0, &ParseError{Oper: "stream entry", Err: errors.New("size too short for name")}
803	}
804
805	names := make([]uint16, sentry.NameLength/2)
806	err = binary.Read(r, binary.LittleEndian, names)
807	if err != nil {
808		return nil, 0, &ParseError{Oper: "file name", Err: err}
809	}
810
811	left -= int64(sentry.NameLength)
812	name := string(utf16.Decode(names))
813
814	var offset resourceDescriptor
815	if sentry.Hash != (SHA1Hash{}) {
816		var ok bool
817		offset, ok = img.wim.fileData[sentry.Hash]
818		if !ok {
819			return nil, 0, &ParseError{Oper: "stream entry", Path: name, Err: fmt.Errorf("could not find file data matching hash %v", sentry.Hash)}
820		}
821	}
822
823	s := &Stream{
824		StreamHeader: StreamHeader{
825			Hash: sentry.Hash,
826			Size: offset.OriginalSize,
827			Name: name,
828		},
829		wim:    img.wim,
830		offset: offset,
831	}
832
833	_, err = io.CopyN(ioutil.Discard, r, left)
834	if err != nil {
835		if err == io.EOF {
836			err = io.ErrUnexpectedEOF
837		}
838		return nil, 0, err
839	}
840
841	return s, length, nil
842}
843
844// Open returns an io.ReadCloser that can be used to read the stream's contents.
845func (s *Stream) Open() (io.ReadCloser, error) {
846	return s.wim.resourceReader(&s.offset)
847}
848
849// Open returns an io.ReadCloser that can be used to read the file's contents.
850func (f *File) Open() (io.ReadCloser, error) {
851	return f.img.wim.resourceReader(&f.offset)
852}
853
854// Readdir reads the directory entries.
855func (f *File) Readdir() ([]*File, error) {
856	if !f.IsDir() {
857		return nil, errors.New("not a directory")
858	}
859	return f.img.readdir(f.subdirOffset)
860}
861
862// IsDir returns whether the given file is a directory. It returns false when it
863// is a directory reparse point.
864func (f *FileHeader) IsDir() bool {
865	return f.Attributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_DIRECTORY
866}
867