1// +build windows
2
3/*
4   Copyright The containerd Authors.
5
6   Licensed under the Apache License, Version 2.0 (the "License");
7   you may not use this file except in compliance with the License.
8   You may obtain a copy of the License at
9
10       http://www.apache.org/licenses/LICENSE-2.0
11
12   Unless required by applicable law or agreed to in writing, software
13   distributed under the License is distributed on an "AS IS" BASIS,
14   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   See the License for the specific language governing permissions and
16   limitations under the License.
17*/
18
19package archive
20
21import (
22	"archive/tar"
23	"bufio"
24	"context"
25	"encoding/base64"
26	"fmt"
27	"io"
28	"os"
29	"path"
30	"path/filepath"
31	"strconv"
32	"strings"
33	"syscall"
34
35	"github.com/Microsoft/go-winio"
36	"github.com/Microsoft/hcsshim"
37	"github.com/containerd/containerd/sys"
38	"github.com/pkg/errors"
39)
40
41const (
42	// MSWINDOWS pax vendor extensions
43	hdrMSWindowsPrefix = "MSWINDOWS."
44
45	hdrFileAttributes        = hdrMSWindowsPrefix + "fileattr"
46	hdrSecurityDescriptor    = hdrMSWindowsPrefix + "sd"
47	hdrRawSecurityDescriptor = hdrMSWindowsPrefix + "rawsd"
48	hdrMountPoint            = hdrMSWindowsPrefix + "mountpoint"
49	hdrEaPrefix              = hdrMSWindowsPrefix + "xattr."
50
51	// LIBARCHIVE pax vendor extensions
52	hdrLibArchivePrefix = "LIBARCHIVE."
53
54	hdrCreateTime = hdrLibArchivePrefix + "creationtime"
55)
56
57var (
58	// mutatedFiles is a list of files that are mutated by the import process
59	// and must be backed up and restored.
60	mutatedFiles = map[string]string{
61		"UtilityVM/Files/EFI/Microsoft/Boot/BCD":      "bcd.bak",
62		"UtilityVM/Files/EFI/Microsoft/Boot/BCD.LOG":  "bcd.log.bak",
63		"UtilityVM/Files/EFI/Microsoft/Boot/BCD.LOG1": "bcd.log1.bak",
64		"UtilityVM/Files/EFI/Microsoft/Boot/BCD.LOG2": "bcd.log2.bak",
65	}
66)
67
68// tarName returns platform-specific filepath
69// to canonical posix-style path for tar archival. p is relative
70// path.
71func tarName(p string) (string, error) {
72	// windows: convert windows style relative path with backslashes
73	// into forward slashes. Since windows does not allow '/' or '\'
74	// in file names, it is mostly safe to replace however we must
75	// check just in case
76	if strings.Contains(p, "/") {
77		return "", fmt.Errorf("windows path contains forward slash: %s", p)
78	}
79
80	return strings.Replace(p, string(os.PathSeparator), "/", -1), nil
81}
82
83// chmodTarEntry is used to adjust the file permissions used in tar header based
84// on the platform the archival is done.
85func chmodTarEntry(perm os.FileMode) os.FileMode {
86	perm &= 0755
87	// Add the x bit: make everything +x from windows
88	perm |= 0111
89
90	return perm
91}
92
93func setHeaderForSpecialDevice(*tar.Header, string, os.FileInfo) error {
94	// do nothing. no notion of Rdev, Inode, Nlink in stat on Windows
95	return nil
96}
97
98func open(p string) (*os.File, error) {
99	// We use sys.OpenSequential to ensure we use sequential file
100	// access on Windows to avoid depleting the standby list.
101	return sys.OpenSequential(p)
102}
103
104func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
105	// Source is regular file. We use sys.OpenFileSequential to use sequential
106	// file access to avoid depleting the standby list on Windows.
107	return sys.OpenFileSequential(name, flag, perm)
108}
109
110func mkdir(path string, perm os.FileMode) error {
111	return os.Mkdir(path, perm)
112}
113
114func skipFile(hdr *tar.Header) bool {
115	// Windows does not support filenames with colons in them. Ignore
116	// these files. This is not a problem though (although it might
117	// appear that it is). Let's suppose a client is running docker pull.
118	// The daemon it points to is Windows. Would it make sense for the
119	// client to be doing a docker pull Ubuntu for example (which has files
120	// with colons in the name under /usr/share/man/man3)? No, absolutely
121	// not as it would really only make sense that they were pulling a
122	// Windows image. However, for development, it is necessary to be able
123	// to pull Linux images which are in the repository.
124	//
125	// TODO Windows. Once the registry is aware of what images are Windows-
126	// specific or Linux-specific, this warning should be changed to an error
127	// to cater for the situation where someone does manage to upload a Linux
128	// image but have it tagged as Windows inadvertently.
129	return strings.Contains(hdr.Name, ":")
130}
131
132// handleTarTypeBlockCharFifo is an OS-specific helper function used by
133// createTarFile to handle the following types of header: Block; Char; Fifo
134func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
135	return nil
136}
137
138func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
139	return nil
140}
141
142func getxattr(path, attr string) ([]byte, error) {
143	return nil, nil
144}
145
146func setxattr(path, key, value string) error {
147	// Return not support error, do not wrap underlying not supported
148	// since xattrs should not exist in windows diff archives
149	return errors.New("xattrs not supported on Windows")
150}
151
152// applyWindowsLayer applies a tar stream of an OCI style diff tar of a Windows
153// layer using the hcsshim layer writer and backup streams.
154// See https://github.com/opencontainers/image-spec/blob/master/layer.md#applying-changesets
155func applyWindowsLayer(ctx context.Context, root string, tr *tar.Reader, options ApplyOptions) (size int64, err error) {
156	home, id := filepath.Split(root)
157	info := hcsshim.DriverInfo{
158		HomeDir: home,
159	}
160
161	w, err := hcsshim.NewLayerWriter(info, id, options.Parents)
162	if err != nil {
163		return 0, err
164	}
165	defer func() {
166		if err2 := w.Close(); err2 != nil {
167			// This error should not be discarded as a failure here
168			// could result in an invalid layer on disk
169			if err == nil {
170				err = err2
171			}
172		}
173	}()
174
175	buf := bufio.NewWriter(nil)
176	hdr, nextErr := tr.Next()
177	// Iterate through the files in the archive.
178	for {
179		select {
180		case <-ctx.Done():
181			return 0, ctx.Err()
182		default:
183		}
184
185		if nextErr == io.EOF {
186			// end of tar archive
187			break
188		}
189		if nextErr != nil {
190			return 0, nextErr
191		}
192
193		// Note: path is used instead of filepath to prevent OS specific handling
194		// of the tar path
195		base := path.Base(hdr.Name)
196		if strings.HasPrefix(base, whiteoutPrefix) {
197			dir := path.Dir(hdr.Name)
198			originalBase := base[len(whiteoutPrefix):]
199			originalPath := path.Join(dir, originalBase)
200			if err := w.Remove(filepath.FromSlash(originalPath)); err != nil {
201				return 0, err
202			}
203			hdr, nextErr = tr.Next()
204		} else if hdr.Typeflag == tar.TypeLink {
205			err := w.AddLink(filepath.FromSlash(hdr.Name), filepath.FromSlash(hdr.Linkname))
206			if err != nil {
207				return 0, err
208			}
209			hdr, nextErr = tr.Next()
210		} else {
211			name, fileSize, fileInfo, err := fileInfoFromHeader(hdr)
212			if err != nil {
213				return 0, err
214			}
215			if err := w.Add(filepath.FromSlash(name), fileInfo); err != nil {
216				return 0, err
217			}
218			size += fileSize
219			hdr, nextErr = tarToBackupStreamWithMutatedFiles(buf, w, tr, hdr, root)
220		}
221	}
222
223	return
224}
225
226// fileInfoFromHeader retrieves basic Win32 file information from a tar header, using the additional metadata written by
227// WriteTarFileFromBackupStream.
228func fileInfoFromHeader(hdr *tar.Header) (name string, size int64, fileInfo *winio.FileBasicInfo, err error) {
229	name = hdr.Name
230	if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA {
231		size = hdr.Size
232	}
233	fileInfo = &winio.FileBasicInfo{
234		LastAccessTime: syscall.NsecToFiletime(hdr.AccessTime.UnixNano()),
235		LastWriteTime:  syscall.NsecToFiletime(hdr.ModTime.UnixNano()),
236		ChangeTime:     syscall.NsecToFiletime(hdr.ChangeTime.UnixNano()),
237
238		// Default CreationTime to ModTime, updated below if MSWINDOWS.createtime exists
239		CreationTime: syscall.NsecToFiletime(hdr.ModTime.UnixNano()),
240	}
241	if attrStr, ok := hdr.PAXRecords[hdrFileAttributes]; ok {
242		attr, err := strconv.ParseUint(attrStr, 10, 32)
243		if err != nil {
244			return "", 0, nil, err
245		}
246		fileInfo.FileAttributes = uint32(attr)
247	} else {
248		if hdr.Typeflag == tar.TypeDir {
249			fileInfo.FileAttributes |= syscall.FILE_ATTRIBUTE_DIRECTORY
250		}
251	}
252	if createStr, ok := hdr.PAXRecords[hdrCreateTime]; ok {
253		createTime, err := parsePAXTime(createStr)
254		if err != nil {
255			return "", 0, nil, err
256		}
257		fileInfo.CreationTime = syscall.NsecToFiletime(createTime.UnixNano())
258	}
259	return
260}
261
262// tarToBackupStreamWithMutatedFiles reads data from a tar stream and
263// writes it to a backup stream, and also saves any files that will be mutated
264// by the import layer process to a backup location.
265func tarToBackupStreamWithMutatedFiles(buf *bufio.Writer, w io.Writer, t *tar.Reader, hdr *tar.Header, root string) (nextHdr *tar.Header, err error) {
266	var (
267		bcdBackup       *os.File
268		bcdBackupWriter *winio.BackupFileWriter
269	)
270	if backupPath, ok := mutatedFiles[hdr.Name]; ok {
271		bcdBackup, err = os.Create(filepath.Join(root, backupPath))
272		if err != nil {
273			return nil, err
274		}
275		defer func() {
276			cerr := bcdBackup.Close()
277			if err == nil {
278				err = cerr
279			}
280		}()
281
282		bcdBackupWriter = winio.NewBackupFileWriter(bcdBackup, false)
283		defer func() {
284			cerr := bcdBackupWriter.Close()
285			if err == nil {
286				err = cerr
287			}
288		}()
289
290		buf.Reset(io.MultiWriter(w, bcdBackupWriter))
291	} else {
292		buf.Reset(w)
293	}
294
295	defer func() {
296		ferr := buf.Flush()
297		if err == nil {
298			err = ferr
299		}
300	}()
301
302	return writeBackupStreamFromTarFile(buf, t, hdr)
303}
304
305// writeBackupStreamFromTarFile writes a Win32 backup stream from the current tar file. Since this function may process multiple
306// tar file entries in order to collect all the alternate data streams for the file, it returns the next
307// tar file that was not processed, or io.EOF is there are no more.
308func writeBackupStreamFromTarFile(w io.Writer, t *tar.Reader, hdr *tar.Header) (*tar.Header, error) {
309	bw := winio.NewBackupStreamWriter(w)
310	var sd []byte
311	var err error
312	// Maintaining old SDDL-based behavior for backward compatibility.  All new tar headers written
313	// by this library will have raw binary for the security descriptor.
314	if sddl, ok := hdr.PAXRecords[hdrSecurityDescriptor]; ok {
315		sd, err = winio.SddlToSecurityDescriptor(sddl)
316		if err != nil {
317			return nil, err
318		}
319	}
320	if sdraw, ok := hdr.PAXRecords[hdrRawSecurityDescriptor]; ok {
321		sd, err = base64.StdEncoding.DecodeString(sdraw)
322		if err != nil {
323			return nil, err
324		}
325	}
326	if len(sd) != 0 {
327		bhdr := winio.BackupHeader{
328			Id:   winio.BackupSecurity,
329			Size: int64(len(sd)),
330		}
331		err := bw.WriteHeader(&bhdr)
332		if err != nil {
333			return nil, err
334		}
335		_, err = bw.Write(sd)
336		if err != nil {
337			return nil, err
338		}
339	}
340	var eas []winio.ExtendedAttribute
341	for k, v := range hdr.PAXRecords {
342		if !strings.HasPrefix(k, hdrEaPrefix) {
343			continue
344		}
345		data, err := base64.StdEncoding.DecodeString(v)
346		if err != nil {
347			return nil, err
348		}
349		eas = append(eas, winio.ExtendedAttribute{
350			Name:  k[len(hdrEaPrefix):],
351			Value: data,
352		})
353	}
354	if len(eas) != 0 {
355		eadata, err := winio.EncodeExtendedAttributes(eas)
356		if err != nil {
357			return nil, err
358		}
359		bhdr := winio.BackupHeader{
360			Id:   winio.BackupEaData,
361			Size: int64(len(eadata)),
362		}
363		err = bw.WriteHeader(&bhdr)
364		if err != nil {
365			return nil, err
366		}
367		_, err = bw.Write(eadata)
368		if err != nil {
369			return nil, err
370		}
371	}
372	if hdr.Typeflag == tar.TypeSymlink {
373		_, isMountPoint := hdr.PAXRecords[hdrMountPoint]
374		rp := winio.ReparsePoint{
375			Target:       filepath.FromSlash(hdr.Linkname),
376			IsMountPoint: isMountPoint,
377		}
378		reparse := winio.EncodeReparsePoint(&rp)
379		bhdr := winio.BackupHeader{
380			Id:   winio.BackupReparseData,
381			Size: int64(len(reparse)),
382		}
383		err := bw.WriteHeader(&bhdr)
384		if err != nil {
385			return nil, err
386		}
387		_, err = bw.Write(reparse)
388		if err != nil {
389			return nil, err
390		}
391	}
392
393	buf := bufPool.Get().(*[]byte)
394	defer bufPool.Put(buf)
395
396	if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA {
397		bhdr := winio.BackupHeader{
398			Id:   winio.BackupData,
399			Size: hdr.Size,
400		}
401		err := bw.WriteHeader(&bhdr)
402		if err != nil {
403			return nil, err
404		}
405		_, err = io.CopyBuffer(bw, t, *buf)
406		if err != nil {
407			return nil, err
408		}
409	}
410	// Copy all the alternate data streams and return the next non-ADS header.
411	for {
412		ahdr, err := t.Next()
413		if err != nil {
414			return nil, err
415		}
416		if ahdr.Typeflag != tar.TypeReg || !strings.HasPrefix(ahdr.Name, hdr.Name+":") {
417			return ahdr, nil
418		}
419		bhdr := winio.BackupHeader{
420			Id:   winio.BackupAlternateData,
421			Size: ahdr.Size,
422			Name: ahdr.Name[len(hdr.Name):] + ":$DATA",
423		}
424		err = bw.WriteHeader(&bhdr)
425		if err != nil {
426			return nil, err
427		}
428		_, err = io.CopyBuffer(bw, t, *buf)
429		if err != nil {
430			return nil, err
431		}
432	}
433}
434
435func copyDirInfo(fi os.FileInfo, path string) error {
436	if err := os.Chmod(path, fi.Mode()); err != nil {
437		return errors.Wrapf(err, "failed to chmod %s", path)
438	}
439	return nil
440}
441
442func copyUpXAttrs(dst, src string) error {
443	return nil
444}
445