Skip to content

Commit 036d9d8

Browse files
authored
Merge pull request #64 from jstarks/tar_ea
Add support for EAs to Windows tars
2 parents 353f597 + 42a2f45 commit 036d9d8

File tree

5 files changed

+328
-6
lines changed

5 files changed

+328
-6
lines changed

backup.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,20 @@ func NewBackupStreamReader(r io.Reader) *BackupStreamReader {
6868
return &BackupStreamReader{r, 0}
6969
}
7070

71-
// Next returns the next backup stream and prepares for calls to Write(). It skips the remainder of the current stream if
71+
// Next returns the next backup stream and prepares for calls to Read(). It skips the remainder of the current stream if
7272
// it was not completely read.
7373
func (r *BackupStreamReader) Next() (*BackupHeader, error) {
7474
if r.bytesLeft > 0 {
75+
if s, ok := r.r.(io.Seeker); ok {
76+
// Make sure Seek on io.SeekCurrent sometimes succeeds
77+
// before trying the actual seek.
78+
if _, err := s.Seek(0, io.SeekCurrent); err == nil {
79+
if _, err = s.Seek(r.bytesLeft, io.SeekCurrent); err != nil {
80+
return nil, err
81+
}
82+
r.bytesLeft = 0
83+
}
84+
}
7585
if _, err := io.Copy(ioutil.Discard, r); err != nil {
7686
return nil, err
7787
}
@@ -220,7 +230,7 @@ type BackupFileWriter struct {
220230
ctx uintptr
221231
}
222232

223-
// NewBackupFileWrtier returns a new BackupFileWriter from a file handle. If includeSecurity is true,
233+
// NewBackupFileWriter returns a new BackupFileWriter from a file handle. If includeSecurity is true,
224234
// Write() will attempt to restore the security descriptor from the stream.
225235
func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
226236
w := &BackupFileWriter{f, includeSecurity, 0}

backuptar/tar.go

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
hdrSecurityDescriptor = "sd"
3737
hdrRawSecurityDescriptor = "rawsd"
3838
hdrMountPoint = "mountpoint"
39+
hdrEaPrefix = "xattr."
3940
)
4041

4142
func writeZeroes(w io.Writer, count int64) error {
@@ -118,6 +119,21 @@ func BasicInfoHeader(name string, size int64, fileInfo *winio.FileBasicInfo) *ta
118119
func WriteTarFileFromBackupStream(t *tar.Writer, r io.Reader, name string, size int64, fileInfo *winio.FileBasicInfo) error {
119120
name = filepath.ToSlash(name)
120121
hdr := BasicInfoHeader(name, size, fileInfo)
122+
123+
// If r can be seeked, then this function is two-pass: pass 1 collects the
124+
// tar header data, and pass 2 copies the data stream. If r cannot be
125+
// seeked, then some header data (in particular EAs) will be silently lost.
126+
var (
127+
restartPos int64
128+
err error
129+
)
130+
sr, readTwice := r.(io.Seeker)
131+
if readTwice {
132+
if restartPos, err = sr.Seek(0, io.SeekCurrent); err != nil {
133+
readTwice = false
134+
}
135+
}
136+
121137
br := winio.NewBackupStreamReader(r)
122138
var dataHdr *winio.BackupHeader
123139
for dataHdr == nil {
@@ -131,7 +147,9 @@ func WriteTarFileFromBackupStream(t *tar.Writer, r io.Reader, name string, size
131147
switch bhdr.Id {
132148
case winio.BackupData:
133149
hdr.Mode |= c_ISREG
134-
dataHdr = bhdr
150+
if !readTwice {
151+
dataHdr = bhdr
152+
}
135153
case winio.BackupSecurity:
136154
sd, err := ioutil.ReadAll(br)
137155
if err != nil {
@@ -151,18 +169,54 @@ func WriteTarFileFromBackupStream(t *tar.Writer, r io.Reader, name string, size
151169
hdr.Winheaders[hdrMountPoint] = "1"
152170
}
153171
hdr.Linkname = rp.Target
154-
case winio.BackupEaData, winio.BackupLink, winio.BackupPropertyData, winio.BackupObjectId, winio.BackupTxfsData:
172+
173+
case winio.BackupEaData:
174+
eab, err := ioutil.ReadAll(br)
175+
if err != nil {
176+
return err
177+
}
178+
eas, err := winio.DecodeExtendedAttributes(eab)
179+
if err != nil {
180+
return err
181+
}
182+
for _, ea := range eas {
183+
// Use base64 encoding for the binary value. Note that there
184+
// is no way to encode the EA's flags, since their use doesn't
185+
// make any sense for persisted EAs.
186+
hdr.Winheaders[hdrEaPrefix+ea.Name] = base64.StdEncoding.EncodeToString(ea.Value)
187+
}
188+
189+
case winio.BackupAlternateData, winio.BackupLink, winio.BackupPropertyData, winio.BackupObjectId, winio.BackupTxfsData:
155190
// ignore these streams
156191
default:
157192
return fmt.Errorf("%s: unknown stream ID %d", name, bhdr.Id)
158193
}
159194
}
160195

161-
err := t.WriteHeader(hdr)
196+
err = t.WriteHeader(hdr)
162197
if err != nil {
163198
return err
164199
}
165200

201+
if readTwice {
202+
// Get back to the data stream.
203+
if _, err = sr.Seek(restartPos, io.SeekStart); err != nil {
204+
return err
205+
}
206+
for dataHdr == nil {
207+
bhdr, err := br.Next()
208+
if err == io.EOF {
209+
break
210+
}
211+
if err != nil {
212+
return err
213+
}
214+
if bhdr.Id == winio.BackupData {
215+
dataHdr = bhdr
216+
}
217+
}
218+
}
219+
166220
if dataHdr != nil {
167221
// A data stream was found. Copy the data.
168222
if (dataHdr.Attributes & winio.StreamSparseAttributes) == 0 {
@@ -293,6 +347,38 @@ func WriteBackupStreamFromTarFile(w io.Writer, t *tar.Reader, hdr *tar.Header) (
293347
return nil, err
294348
}
295349
}
350+
var eas []winio.ExtendedAttribute
351+
for k, v := range hdr.Winheaders {
352+
if !strings.HasPrefix(k, hdrEaPrefix) {
353+
continue
354+
}
355+
data, err := base64.StdEncoding.DecodeString(v)
356+
if err != nil {
357+
return nil, err
358+
}
359+
eas = append(eas, winio.ExtendedAttribute{
360+
Name: k[len(hdrEaPrefix):],
361+
Value: data,
362+
})
363+
}
364+
if len(eas) != 0 {
365+
eadata, err := winio.EncodeExtendedAttributes(eas)
366+
if err != nil {
367+
return nil, err
368+
}
369+
bhdr := winio.BackupHeader{
370+
Id: winio.BackupEaData,
371+
Size: int64(len(eadata)),
372+
}
373+
err = bw.WriteHeader(&bhdr)
374+
if err != nil {
375+
return nil, err
376+
}
377+
_, err = bw.Write(eadata)
378+
if err != nil {
379+
return nil, err
380+
}
381+
}
296382
if hdr.Typeflag == tar.TypeSymlink {
297383
_, isMountPoint := hdr.Winheaders[hdrMountPoint]
298384
rp := winio.ReparsePoint{

backuptar/tar_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,5 +80,5 @@ func TestRoundTrip(t *testing.T) {
8080
t.Errorf("got %#v, expected %#v", *bi, *bi2)
8181
}
8282

83-
ensurePresent(t, hdr.Winheaders, "fileattr", "sd")
83+
ensurePresent(t, hdr.Winheaders, "fileattr", "rawsd")
8484
}

ea.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package winio
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"errors"
7+
)
8+
9+
type fileFullEaInformation struct {
10+
NextEntryOffset uint32
11+
Flags uint8
12+
NameLength uint8
13+
ValueLength uint16
14+
}
15+
16+
var (
17+
fileFullEaInformationSize = binary.Size(&fileFullEaInformation{})
18+
19+
errInvalidEaBuffer = errors.New("invalid extended attribute buffer")
20+
errEaNameTooLarge = errors.New("extended attribute name too large")
21+
errEaValueTooLarge = errors.New("extended attribute value too large")
22+
)
23+
24+
// ExtendedAttribute represents a single Windows EA.
25+
type ExtendedAttribute struct {
26+
Name string
27+
Value []byte
28+
Flags uint8
29+
}
30+
31+
func parseEa(b []byte) (ea ExtendedAttribute, nb []byte, err error) {
32+
var info fileFullEaInformation
33+
err = binary.Read(bytes.NewReader(b), binary.LittleEndian, &info)
34+
if err != nil {
35+
err = errInvalidEaBuffer
36+
return
37+
}
38+
39+
nameOffset := fileFullEaInformationSize
40+
nameLen := int(info.NameLength)
41+
valueOffset := nameOffset + int(info.NameLength) + 1
42+
valueLen := int(info.ValueLength)
43+
nextOffset := int(info.NextEntryOffset)
44+
if valueLen+valueOffset > len(b) || nextOffset < 0 || nextOffset > len(b) {
45+
err = errInvalidEaBuffer
46+
return
47+
}
48+
49+
ea.Name = string(b[nameOffset : nameOffset+nameLen])
50+
ea.Value = b[valueOffset : valueOffset+valueLen]
51+
ea.Flags = info.Flags
52+
if info.NextEntryOffset != 0 {
53+
nb = b[info.NextEntryOffset:]
54+
}
55+
return
56+
}
57+
58+
// DecodeExtendedAttributes decodes a list of EAs from a FILE_FULL_EA_INFORMATION
59+
// buffer retrieved from BackupRead, ZwQueryEaFile, etc.
60+
func DecodeExtendedAttributes(b []byte) (eas []ExtendedAttribute, err error) {
61+
for len(b) != 0 {
62+
ea, nb, err := parseEa(b)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
eas = append(eas, ea)
68+
b = nb
69+
}
70+
return
71+
}
72+
73+
func writeEa(buf *bytes.Buffer, ea *ExtendedAttribute, last bool) error {
74+
if int(uint8(len(ea.Name))) != len(ea.Name) {
75+
return errEaNameTooLarge
76+
}
77+
if int(uint16(len(ea.Value))) != len(ea.Value) {
78+
return errEaValueTooLarge
79+
}
80+
entrySize := uint32(fileFullEaInformationSize + len(ea.Name) + 1 + len(ea.Value))
81+
withPadding := (entrySize + 3) &^ 3
82+
nextOffset := uint32(0)
83+
if !last {
84+
nextOffset = withPadding
85+
}
86+
info := fileFullEaInformation{
87+
NextEntryOffset: nextOffset,
88+
Flags: ea.Flags,
89+
NameLength: uint8(len(ea.Name)),
90+
ValueLength: uint16(len(ea.Value)),
91+
}
92+
93+
err := binary.Write(buf, binary.LittleEndian, &info)
94+
if err != nil {
95+
return err
96+
}
97+
98+
_, err = buf.Write([]byte(ea.Name))
99+
if err != nil {
100+
return err
101+
}
102+
103+
err = buf.WriteByte(0)
104+
if err != nil {
105+
return err
106+
}
107+
108+
_, err = buf.Write(ea.Value)
109+
if err != nil {
110+
return err
111+
}
112+
113+
_, err = buf.Write([]byte{0, 0, 0}[0 : withPadding-entrySize])
114+
if err != nil {
115+
return err
116+
}
117+
118+
return nil
119+
}
120+
121+
// EncodeExtendedAttributes encodes a list of EAs into a FILE_FULL_EA_INFORMATION
122+
// buffer for use with BackupWrite, ZwSetEaFile, etc.
123+
func EncodeExtendedAttributes(eas []ExtendedAttribute) ([]byte, error) {
124+
var buf bytes.Buffer
125+
for i := range eas {
126+
last := false
127+
if i == len(eas)-1 {
128+
last = true
129+
}
130+
131+
err := writeEa(&buf, &eas[i], last)
132+
if err != nil {
133+
return nil, err
134+
}
135+
}
136+
return buf.Bytes(), nil
137+
}

0 commit comments

Comments
 (0)