1// +build windows
2
3package backuptar
4
5import (
6	"archive/tar"
7	"encoding/base64"
8	"errors"
9	"fmt"
10	"io"
11	"io/ioutil"
12	"path/filepath"
13	"strconv"
14	"strings"
15	"syscall"
16	"time"
17
18	"github.com/Microsoft/go-winio"
19	"golang.org/x/sys/windows"
20)
21
22const (
23	c_ISUID  = 04000   // Set uid
24	c_ISGID  = 02000   // Set gid
25	c_ISVTX  = 01000   // Save text (sticky bit)
26	c_ISDIR  = 040000  // Directory
27	c_ISFIFO = 010000  // FIFO
28	c_ISREG  = 0100000 // Regular file
29	c_ISLNK  = 0120000 // Symbolic link
30	c_ISBLK  = 060000  // Block special file
31	c_ISCHR  = 020000  // Character special file
32	c_ISSOCK = 0140000 // Socket
33)
34
35const (
36	hdrFileAttributes        = "MSWINDOWS.fileattr"
37	hdrSecurityDescriptor    = "MSWINDOWS.sd"
38	hdrRawSecurityDescriptor = "MSWINDOWS.rawsd"
39	hdrMountPoint            = "MSWINDOWS.mountpoint"
40	hdrEaPrefix              = "MSWINDOWS.xattr."
41
42	hdrCreationTime = "LIBARCHIVE.creationtime"
43)
44
45func writeZeroes(w io.Writer, count int64) error {
46	buf := make([]byte, 8192)
47	c := len(buf)
48	for i := int64(0); i < count; i += int64(c) {
49		if int64(c) > count-i {
50			c = int(count - i)
51		}
52		_, err := w.Write(buf[:c])
53		if err != nil {
54			return err
55		}
56	}
57	return nil
58}
59
60func copySparse(t *tar.Writer, br *winio.BackupStreamReader) error {
61	curOffset := int64(0)
62	for {
63		bhdr, err := br.Next()
64		if err == io.EOF {
65			err = io.ErrUnexpectedEOF
66		}
67		if err != nil {
68			return err
69		}
70		if bhdr.Id != winio.BackupSparseBlock {
71			return fmt.Errorf("unexpected stream %d", bhdr.Id)
72		}
73
74		// archive/tar does not support writing sparse files
75		// so just write zeroes to catch up to the current offset.
76		err = writeZeroes(t, bhdr.Offset-curOffset)
77		if bhdr.Size == 0 {
78			break
79		}
80		n, err := io.Copy(t, br)
81		if err != nil {
82			return err
83		}
84		curOffset = bhdr.Offset + n
85	}
86	return nil
87}
88
89// BasicInfoHeader creates a tar header from basic file information.
90func BasicInfoHeader(name string, size int64, fileInfo *winio.FileBasicInfo) *tar.Header {
91	hdr := &tar.Header{
92		Format:     tar.FormatPAX,
93		Name:       filepath.ToSlash(name),
94		Size:       size,
95		Typeflag:   tar.TypeReg,
96		ModTime:    time.Unix(0, fileInfo.LastWriteTime.Nanoseconds()),
97		ChangeTime: time.Unix(0, fileInfo.ChangeTime.Nanoseconds()),
98		AccessTime: time.Unix(0, fileInfo.LastAccessTime.Nanoseconds()),
99		PAXRecords: make(map[string]string),
100	}
101	hdr.PAXRecords[hdrFileAttributes] = fmt.Sprintf("%d", fileInfo.FileAttributes)
102	hdr.PAXRecords[hdrCreationTime] = formatPAXTime(time.Unix(0, fileInfo.CreationTime.Nanoseconds()))
103
104	if (fileInfo.FileAttributes & syscall.FILE_ATTRIBUTE_DIRECTORY) != 0 {
105		hdr.Mode |= c_ISDIR
106		hdr.Size = 0
107		hdr.Typeflag = tar.TypeDir
108	}
109	return hdr
110}
111
112// WriteTarFileFromBackupStream writes a file to a tar writer using data from a Win32 backup stream.
113//
114// This encodes Win32 metadata as tar pax vendor extensions starting with MSWINDOWS.
115//
116// The additional Win32 metadata is:
117//
118// MSWINDOWS.fileattr: The Win32 file attributes, as a decimal value
119//
120// MSWINDOWS.rawsd: The Win32 security descriptor, in raw binary format
121//
122// MSWINDOWS.mountpoint: If present, this is a mount point and not a symlink, even though the type is '2' (symlink)
123func WriteTarFileFromBackupStream(t *tar.Writer, r io.Reader, name string, size int64, fileInfo *winio.FileBasicInfo) error {
124	name = filepath.ToSlash(name)
125	hdr := BasicInfoHeader(name, size, fileInfo)
126
127	// If r can be seeked, then this function is two-pass: pass 1 collects the
128	// tar header data, and pass 2 copies the data stream. If r cannot be
129	// seeked, then some header data (in particular EAs) will be silently lost.
130	var (
131		restartPos int64
132		err        error
133	)
134	sr, readTwice := r.(io.Seeker)
135	if readTwice {
136		if restartPos, err = sr.Seek(0, io.SeekCurrent); err != nil {
137			readTwice = false
138		}
139	}
140
141	br := winio.NewBackupStreamReader(r)
142	var dataHdr *winio.BackupHeader
143	for dataHdr == nil {
144		bhdr, err := br.Next()
145		if err == io.EOF {
146			break
147		}
148		if err != nil {
149			return err
150		}
151		switch bhdr.Id {
152		case winio.BackupData:
153			hdr.Mode |= c_ISREG
154			if !readTwice {
155				dataHdr = bhdr
156			}
157		case winio.BackupSecurity:
158			sd, err := ioutil.ReadAll(br)
159			if err != nil {
160				return err
161			}
162			hdr.PAXRecords[hdrRawSecurityDescriptor] = base64.StdEncoding.EncodeToString(sd)
163
164		case winio.BackupReparseData:
165			hdr.Mode |= c_ISLNK
166			hdr.Typeflag = tar.TypeSymlink
167			reparseBuffer, err := ioutil.ReadAll(br)
168			rp, err := winio.DecodeReparsePoint(reparseBuffer)
169			if err != nil {
170				return err
171			}
172			if rp.IsMountPoint {
173				hdr.PAXRecords[hdrMountPoint] = "1"
174			}
175			hdr.Linkname = rp.Target
176
177		case winio.BackupEaData:
178			eab, err := ioutil.ReadAll(br)
179			if err != nil {
180				return err
181			}
182			eas, err := winio.DecodeExtendedAttributes(eab)
183			if err != nil {
184				return err
185			}
186			for _, ea := range eas {
187				// Use base64 encoding for the binary value. Note that there
188				// is no way to encode the EA's flags, since their use doesn't
189				// make any sense for persisted EAs.
190				hdr.PAXRecords[hdrEaPrefix+ea.Name] = base64.StdEncoding.EncodeToString(ea.Value)
191			}
192
193		case winio.BackupAlternateData, winio.BackupLink, winio.BackupPropertyData, winio.BackupObjectId, winio.BackupTxfsData:
194			// ignore these streams
195		default:
196			return fmt.Errorf("%s: unknown stream ID %d", name, bhdr.Id)
197		}
198	}
199
200	err = t.WriteHeader(hdr)
201	if err != nil {
202		return err
203	}
204
205	if readTwice {
206		// Get back to the data stream.
207		if _, err = sr.Seek(restartPos, io.SeekStart); err != nil {
208			return err
209		}
210		for dataHdr == nil {
211			bhdr, err := br.Next()
212			if err == io.EOF {
213				break
214			}
215			if err != nil {
216				return err
217			}
218			if bhdr.Id == winio.BackupData {
219				dataHdr = bhdr
220			}
221		}
222	}
223
224	if dataHdr != nil {
225		// A data stream was found. Copy the data.
226		if (dataHdr.Attributes & winio.StreamSparseAttributes) == 0 {
227			if size != dataHdr.Size {
228				return fmt.Errorf("%s: mismatch between file size %d and header size %d", name, size, dataHdr.Size)
229			}
230			_, err = io.Copy(t, br)
231			if err != nil {
232				return err
233			}
234		} else {
235			err = copySparse(t, br)
236			if err != nil {
237				return err
238			}
239		}
240	}
241
242	// Look for streams after the data stream. The only ones we handle are alternate data streams.
243	// Other streams may have metadata that could be serialized, but the tar header has already
244	// been written. In practice, this means that we don't get EA or TXF metadata.
245	for {
246		bhdr, err := br.Next()
247		if err == io.EOF {
248			break
249		}
250		if err != nil {
251			return err
252		}
253		switch bhdr.Id {
254		case winio.BackupAlternateData:
255			altName := bhdr.Name
256			if strings.HasSuffix(altName, ":$DATA") {
257				altName = altName[:len(altName)-len(":$DATA")]
258			}
259			if (bhdr.Attributes & winio.StreamSparseAttributes) == 0 {
260				hdr = &tar.Header{
261					Format:     hdr.Format,
262					Name:       name + altName,
263					Mode:       hdr.Mode,
264					Typeflag:   tar.TypeReg,
265					Size:       bhdr.Size,
266					ModTime:    hdr.ModTime,
267					AccessTime: hdr.AccessTime,
268					ChangeTime: hdr.ChangeTime,
269				}
270				err = t.WriteHeader(hdr)
271				if err != nil {
272					return err
273				}
274				_, err = io.Copy(t, br)
275				if err != nil {
276					return err
277				}
278
279			} else {
280				// Unsupported for now, since the size of the alternate stream is not present
281				// in the backup stream until after the data has been read.
282				return errors.New("tar of sparse alternate data streams is unsupported")
283			}
284		case winio.BackupEaData, winio.BackupLink, winio.BackupPropertyData, winio.BackupObjectId, winio.BackupTxfsData:
285			// ignore these streams
286		default:
287			return fmt.Errorf("%s: unknown stream ID %d after data", name, bhdr.Id)
288		}
289	}
290	return nil
291}
292
293// FileInfoFromHeader retrieves basic Win32 file information from a tar header, using the additional metadata written by
294// WriteTarFileFromBackupStream.
295func FileInfoFromHeader(hdr *tar.Header) (name string, size int64, fileInfo *winio.FileBasicInfo, err error) {
296	name = hdr.Name
297	if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA {
298		size = hdr.Size
299	}
300	fileInfo = &winio.FileBasicInfo{
301		LastAccessTime: windows.NsecToFiletime(hdr.AccessTime.UnixNano()),
302		LastWriteTime:  windows.NsecToFiletime(hdr.ModTime.UnixNano()),
303		ChangeTime:     windows.NsecToFiletime(hdr.ChangeTime.UnixNano()),
304		// Default to ModTime, we'll pull hdrCreationTime below if present
305		CreationTime: windows.NsecToFiletime(hdr.ModTime.UnixNano()),
306	}
307	if attrStr, ok := hdr.PAXRecords[hdrFileAttributes]; ok {
308		attr, err := strconv.ParseUint(attrStr, 10, 32)
309		if err != nil {
310			return "", 0, nil, err
311		}
312		fileInfo.FileAttributes = uint32(attr)
313	} else {
314		if hdr.Typeflag == tar.TypeDir {
315			fileInfo.FileAttributes |= syscall.FILE_ATTRIBUTE_DIRECTORY
316		}
317	}
318	if creationTimeStr, ok := hdr.PAXRecords[hdrCreationTime]; ok {
319		creationTime, err := parsePAXTime(creationTimeStr)
320		if err != nil {
321			return "", 0, nil, err
322		}
323		fileInfo.CreationTime = windows.NsecToFiletime(creationTime.UnixNano())
324	}
325	return
326}
327
328// WriteBackupStreamFromTarFile writes a Win32 backup stream from the current tar file. Since this function may process multiple
329// tar file entries in order to collect all the alternate data streams for the file, it returns the next
330// tar file that was not processed, or io.EOF is there are no more.
331func WriteBackupStreamFromTarFile(w io.Writer, t *tar.Reader, hdr *tar.Header) (*tar.Header, error) {
332	bw := winio.NewBackupStreamWriter(w)
333	var sd []byte
334	var err error
335	// Maintaining old SDDL-based behavior for backward compatibility.  All new tar headers written
336	// by this library will have raw binary for the security descriptor.
337	if sddl, ok := hdr.PAXRecords[hdrSecurityDescriptor]; ok {
338		sd, err = winio.SddlToSecurityDescriptor(sddl)
339		if err != nil {
340			return nil, err
341		}
342	}
343	if sdraw, ok := hdr.PAXRecords[hdrRawSecurityDescriptor]; ok {
344		sd, err = base64.StdEncoding.DecodeString(sdraw)
345		if err != nil {
346			return nil, err
347		}
348	}
349	if len(sd) != 0 {
350		bhdr := winio.BackupHeader{
351			Id:   winio.BackupSecurity,
352			Size: int64(len(sd)),
353		}
354		err := bw.WriteHeader(&bhdr)
355		if err != nil {
356			return nil, err
357		}
358		_, err = bw.Write(sd)
359		if err != nil {
360			return nil, err
361		}
362	}
363	var eas []winio.ExtendedAttribute
364	for k, v := range hdr.PAXRecords {
365		if !strings.HasPrefix(k, hdrEaPrefix) {
366			continue
367		}
368		data, err := base64.StdEncoding.DecodeString(v)
369		if err != nil {
370			return nil, err
371		}
372		eas = append(eas, winio.ExtendedAttribute{
373			Name:  k[len(hdrEaPrefix):],
374			Value: data,
375		})
376	}
377	if len(eas) != 0 {
378		eadata, err := winio.EncodeExtendedAttributes(eas)
379		if err != nil {
380			return nil, err
381		}
382		bhdr := winio.BackupHeader{
383			Id:   winio.BackupEaData,
384			Size: int64(len(eadata)),
385		}
386		err = bw.WriteHeader(&bhdr)
387		if err != nil {
388			return nil, err
389		}
390		_, err = bw.Write(eadata)
391		if err != nil {
392			return nil, err
393		}
394	}
395	if hdr.Typeflag == tar.TypeSymlink {
396		_, isMountPoint := hdr.PAXRecords[hdrMountPoint]
397		rp := winio.ReparsePoint{
398			Target:       filepath.FromSlash(hdr.Linkname),
399			IsMountPoint: isMountPoint,
400		}
401		reparse := winio.EncodeReparsePoint(&rp)
402		bhdr := winio.BackupHeader{
403			Id:   winio.BackupReparseData,
404			Size: int64(len(reparse)),
405		}
406		err := bw.WriteHeader(&bhdr)
407		if err != nil {
408			return nil, err
409		}
410		_, err = bw.Write(reparse)
411		if err != nil {
412			return nil, err
413		}
414	}
415	if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA {
416		bhdr := winio.BackupHeader{
417			Id:   winio.BackupData,
418			Size: hdr.Size,
419		}
420		err := bw.WriteHeader(&bhdr)
421		if err != nil {
422			return nil, err
423		}
424		_, err = io.Copy(bw, t)
425		if err != nil {
426			return nil, err
427		}
428	}
429	// Copy all the alternate data streams and return the next non-ADS header.
430	for {
431		ahdr, err := t.Next()
432		if err != nil {
433			return nil, err
434		}
435		if ahdr.Typeflag != tar.TypeReg || !strings.HasPrefix(ahdr.Name, hdr.Name+":") {
436			return ahdr, nil
437		}
438		bhdr := winio.BackupHeader{
439			Id:   winio.BackupAlternateData,
440			Size: ahdr.Size,
441			Name: ahdr.Name[len(hdr.Name):] + ":$DATA",
442		}
443		err = bw.WriteHeader(&bhdr)
444		if err != nil {
445			return nil, err
446		}
447		_, err = io.Copy(bw, t)
448		if err != nil {
449			return nil, err
450		}
451	}
452}
453