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