diff --git a/internal/dump/acl.go b/internal/dump/acl.go index 48ce28a4c..2735ffa46 100644 --- a/internal/dump/acl.go +++ b/internal/dump/acl.go @@ -1,131 +1,88 @@ package dump -// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go - import ( - "bytes" "encoding/binary" - "fmt" + "errors" + "strconv" ) const ( - aclUserOwner = 0x0001 - aclUser = 0x0002 - aclGroupOwner = 0x0004 - aclGroup = 0x0008 - aclMask = 0x0010 - aclOthers = 0x0020 + // Permissions + aclPermRead = 0x4 + aclPermWrite = 0x2 + aclPermExecute = 0x1 + + // Tags + aclTagUserObj = 0x01 // Owner. + aclTagUser = 0x02 + aclTagGroupObj = 0x04 // Owning group. + aclTagGroup = 0x08 + aclTagMask = 0x10 + aclTagOther = 0x20 ) -type aclSID uint64 - -type aclElem struct { - Tag uint16 - Perm uint16 - ID uint32 -} - -type acl struct { - Version uint32 - List []aclElement -} - -type aclElement struct { - aclSID - Perm uint16 -} - -func (a aclSID) getType() int { - return int(a >> 32) -} -func (a aclSID) getID() uint32 { - return uint32(a & 0xffffffff) -} -func (a aclSID) String() string { - switch a >> 32 { - case aclUserOwner: - return "user::" - case aclUser: - return fmt.Sprintf("user:%v:", a.getID()) - case aclGroupOwner: - return "group::" - case aclGroup: - return fmt.Sprintf("group:%v:", a.getID()) - case aclMask: - return "mask::" - case aclOthers: - return "other::" +// formatLinuxACL converts a Linux ACL from its binary format to the POSIX.1e +// long text format. +// +// User and group IDs are printed in decimal, because we may be dumping +// a snapshot from a different machine. +// +// https://man7.org/linux/man-pages/man5/acl.5.html +// https://savannah.nongnu.org/projects/acl +// https://simson.net/ref/1997/posix_1003.1e-990310.pdf +func formatLinuxACL(acl []byte) (string, error) { + if len(acl)-4 < 0 || (len(acl)-4)%8 != 0 { + return "", errors.New("wrong length") } - return "?:" -} + version := binary.LittleEndian.Uint32(acl) + if version != 2 { + return "", errors.New("unsupported ACL format version") + } + acl = acl[4:] -func (a aclElement) String() string { - str := "" - if (a.Perm & 4) != 0 { - str += "r" - } else { - str += "-" - } - if (a.Perm & 2) != 0 { - str += "w" - } else { - str += "-" - } - if (a.Perm & 1) != 0 { - str += "x" - } else { - str += "-" - } - return fmt.Sprintf("%v%v", a.aclSID, str) -} + text := make([]byte, 0, 2*len(acl)) -func (a *acl) decode(xattr []byte) { - var elem aclElement - ae := new(aclElem) - nr := bytes.NewReader(xattr) - e := binary.Read(nr, binary.LittleEndian, &a.Version) - if e != nil { - a.Version = 0 - return - } - if len(a.List) > 0 { - a.List = a.List[:0] - } - for binary.Read(nr, binary.LittleEndian, ae) == nil { - elem.aclSID = (aclSID(ae.Tag) << 32) | aclSID(ae.ID) - elem.Perm = ae.Perm - a.List = append(a.List, elem) - } -} + for ; len(acl) >= 8; acl = acl[8:] { + tag := binary.LittleEndian.Uint16(acl) + perm := binary.LittleEndian.Uint16(acl[2:]) + id := binary.LittleEndian.Uint32(acl[4:]) -func (a *acl) encode() []byte { - buf := new(bytes.Buffer) - ae := new(aclElem) - - err := binary.Write(buf, binary.LittleEndian, &a.Version) - // write to a bytes.Buffer always returns a nil error - if err != nil { - panic(err) - } - - for _, elem := range a.List { - ae.Tag = uint16(elem.getType()) - ae.Perm = elem.Perm - ae.ID = elem.getID() - - err := binary.Write(buf, binary.LittleEndian, ae) - // write to a bytes.Buffer always returns a nil error - if err != nil { - panic(err) + switch tag { + case aclTagUserObj: + text = append(text, "user:"...) + case aclTagUser: + text = append(text, "user:"...) + text = strconv.AppendUint(text, uint64(id), 10) + case aclTagGroupObj: + text = append(text, "group:"...) + case aclTagGroup: + text = append(text, "group:"...) + text = strconv.AppendUint(text, uint64(id), 10) + case aclTagMask: + text = append(text, "mask:"...) + case aclTagOther: + text = append(text, "other:"...) + default: + return "", errors.New("unknown tag") } + text = append(text, ':') + text = append(text, aclPermText(perm)...) + text = append(text, '\n') } - return buf.Bytes() + + return string(text), nil } -func (a *acl) String() string { - var finalacl string - for _, acl := range a.List { - finalacl += acl.String() + "\n" +func aclPermText(p uint16) []byte { + s := []byte("---") + if p&aclPermRead != 0 { + s[0] = 'r' } - return finalacl + if p&aclPermWrite != 0 { + s[1] = 'w' + } + if p&aclPermExecute != 0 { + s[2] = 'x' + } + return s } diff --git a/internal/dump/acl_test.go b/internal/dump/acl_test.go index bef11ad14..658850147 100644 --- a/internal/dump/acl_test.go +++ b/internal/dump/acl_test.go @@ -1,114 +1,46 @@ package dump import ( - "reflect" "testing" + + rtest "github.com/restic/restic/internal/test" ) -func Test_acl_decode(t *testing.T) { - type args struct { - xattr []byte - } - tests := []struct { - name string - args args - want string +func TestFormatLinuxACL(t *testing.T) { + for _, c := range []struct { + in, out, err string }{ { - name: "decode string", - args: args{ - xattr: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255}, - }, - want: "user::rw-\nuser:0:rwx\nuser:65534:rwx\ngroup::rwx\nmask::rwx\nother::r--\n", + in: "\x02\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" + + "\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" + + "\x04\x00\x02\x00\xff\xff\xff\xff\b\x00\x01\x00'\x00\x00\x00" + + "\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff", + out: "user::rw-\nuser:3:r--\nuser:1001:r--\ngroup::-w-\n" + + "group:39:--x\nmask::rwx\nother::r--\n", }, { - name: "decode group", - args: args{ - xattr: []byte{2, 0, 0, 0, 8, 0, 1, 0, 254, 255, 0, 0}, - }, - want: "group:65534:--x\n", + in: "\x02\x00\x00\x00\x00\x00\x06\x00\xff\xff\xff\xff\x02\x00" + + "\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" + + "\x04\x00\x06\x00\xff\xff\xff\xff\b\x00\x05\x00'\x00\x00\x00" + + "\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff", + err: "unknown tag", }, { - name: "decode fail", - args: args{ - xattr: []byte("abctest"), - }, - want: "", + in: "\x01\x00\x00\x00\x01\x00\x06\x00\xff\xff\xff\xff\x02\x00" + + "\x04\x00\x03\x00\x00\x00\x02\x00\x04\x00\xe9\x03\x00\x00" + + "\x04\x00\x06\x00\xff\xff\xff\xff\b\x00\x05\x00'\x00\x00\x00" + + "\x10\x00\a\x00\xff\xff\xff\xff \x00\x04\x00\xff\xff\xff\xff", + err: "unsupported ACL format version", }, - { - name: "decode empty fail", - args: args{ - xattr: []byte(""), - }, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := &acl{} - a.decode(tt.args.xattr) - if tt.want != a.String() { - t.Errorf("acl.decode() = %v, want: %v", a.String(), tt.want) - } - a.decode(tt.args.xattr) - if tt.want != a.String() { - t.Errorf("second acl.decode() = %v, want: %v", a.String(), tt.want) - } - }) - } -} - -func Test_acl_encode(t *testing.T) { - tests := []struct { - name string - want []byte - args []aclElement - }{ - { - name: "encode values", - want: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255}, - args: []aclElement{ - { - aclSID: 8589934591, - Perm: 6, - }, - { - aclSID: 8589934592, - Perm: 7, - }, - { - aclSID: 8590000126, - Perm: 7, - }, - { - aclSID: 21474836479, - Perm: 7, - }, - { - aclSID: 73014444031, - Perm: 7, - }, - { - aclSID: 141733920767, - Perm: 4, - }, - }, - }, - { - name: "encode fail", - want: []byte{2, 0, 0, 0}, - args: []aclElement{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := &acl{ - Version: 2, - List: tt.args, - } - if got := a.encode(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("acl.encode() = %v, want %v", got, tt.want) - } - }) + {in: "\x02\x00", err: "wrong length"}, + {in: "", err: "wrong length"}, + } { + out, err := formatLinuxACL([]byte(c.in)) + if c.err == "" { + rtest.Equals(t, c.out, out) + } else { + rtest.Assert(t, err != nil, "wanted %q but got nil", c.err) + rtest.Equals(t, c.err, err.Error()) + } } } diff --git a/internal/dump/tar.go b/internal/dump/tar.go index df9ea429d..e8f34deb1 100644 --- a/internal/dump/tar.go +++ b/internal/dump/tar.go @@ -6,8 +6,8 @@ import ( "fmt" "os" "path/filepath" - "strings" + "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" ) @@ -104,21 +104,28 @@ func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string { tmpMap := make(map[string]string) for _, attr := range xattrs { - attrString := string(attr.Value) + // Check for Linux POSIX.1e ACLs. + // + // TODO support ACLs from other operating systems. + // FreeBSD ACLs have names "posix1e.acl_(access|default)", + // but their binary format may not match the Linux format. + aclKey := "" + switch attr.Name { + case "system.posix_acl_access": + aclKey = "SCHILY.acl.access" + case "system.posix_acl_default": + aclKey = "SCHILY.acl.default" + } - if strings.HasPrefix(attr.Name, "system.posix_acl_") { - na := acl{} - na.decode(attr.Value) - - if na.String() != "" { - if strings.Contains(attr.Name, "system.posix_acl_access") { - tmpMap["SCHILY.acl.access"] = na.String() - } else if strings.Contains(attr.Name, "system.posix_acl_default") { - tmpMap["SCHILY.acl.default"] = na.String() - } + if aclKey != "" { + text, err := formatLinuxACL(attr.Value) + if err != nil { + debug.Log("parsing Linux ACL: %v, skipping", err) + continue } + tmpMap[aclKey] = text } else { - tmpMap["SCHILY.xattr."+attr.Name] = attrString + tmpMap["SCHILY.xattr."+attr.Name] = string(attr.Value) } }