mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
4e72992900
As a fallback to package managers, allow updating tailscale that was self-installed in some way. There are some tricky bits around updating the systemd unit (should we stick to local binary paths or to the ones in tailscaled.service?), so leaving that out for now. Updates #6995 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
764 lines
18 KiB
Go
764 lines
18 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package clientupdate
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io/fs"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
toTrack string
|
|
in string
|
|
want string // empty means want no change
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "stable-to-unstable",
|
|
toTrack: UnstableTrack,
|
|
in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
|
|
want: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
|
|
},
|
|
{
|
|
name: "stable-unchanged",
|
|
toTrack: StableTrack,
|
|
in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
|
|
},
|
|
{
|
|
name: "if-both-stable-and-unstable-dont-change",
|
|
toTrack: StableTrack,
|
|
in: "# Tailscale packages for debian buster\n" +
|
|
"deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
|
|
"deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
|
|
},
|
|
{
|
|
name: "if-both-stable-and-unstable-dont-change-unstable",
|
|
toTrack: UnstableTrack,
|
|
in: "# Tailscale packages for debian buster\n" +
|
|
"deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
|
|
"deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
|
|
},
|
|
{
|
|
name: "signed-by-form",
|
|
toTrack: UnstableTrack,
|
|
in: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu jammy main\n",
|
|
want: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/unstable/ubuntu jammy main\n",
|
|
},
|
|
{
|
|
name: "unsupported-lines",
|
|
toTrack: UnstableTrack,
|
|
in: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/foobar/ubuntu jammy main\n",
|
|
wantErr: "unexpected/unsupported /etc/apt/sources.list.d/tailscale.list contents",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
newContent, err := updateDebianAptSourcesListBytes([]byte(tt.in), tt.toTrack)
|
|
if err != nil {
|
|
if err.Error() != tt.wantErr {
|
|
t.Fatalf("error = %v; want %q", err, tt.wantErr)
|
|
}
|
|
return
|
|
}
|
|
if tt.wantErr != "" {
|
|
t.Fatalf("got no error; want %q", tt.wantErr)
|
|
}
|
|
var gotChange string
|
|
if string(newContent) != tt.in {
|
|
gotChange = string(newContent)
|
|
}
|
|
if gotChange != tt.want {
|
|
t.Errorf("wrong result\n got: %q\nwant: %q", gotChange, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSoftwareupdateList(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []byte
|
|
want string
|
|
}{
|
|
{
|
|
name: "update-at-end-of-list",
|
|
input: []byte(`
|
|
Software Update Tool
|
|
|
|
Finding available software
|
|
Software Update found the following new or updated software:
|
|
* Label: MacBookAirEFIUpdate2.4-2.4
|
|
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
|
* Label: ProAppsQTCodecs-1.0
|
|
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
|
* Label: Tailscale-1.23.4
|
|
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
|
|
`),
|
|
want: "Tailscale-1.23.4",
|
|
},
|
|
{
|
|
name: "update-in-middle-of-list",
|
|
input: []byte(`
|
|
Software Update Tool
|
|
|
|
Finding available software
|
|
Software Update found the following new or updated software:
|
|
* Label: MacBookAirEFIUpdate2.4-2.4
|
|
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
|
* Label: Tailscale-1.23.5000
|
|
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
|
|
* Label: ProAppsQTCodecs-1.0
|
|
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
|
`),
|
|
want: "Tailscale-1.23.5000",
|
|
},
|
|
{
|
|
name: "update-not-in-list",
|
|
input: []byte(`
|
|
Software Update Tool
|
|
|
|
Finding available software
|
|
Software Update found the following new or updated software:
|
|
* Label: MacBookAirEFIUpdate2.4-2.4
|
|
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
|
* Label: ProAppsQTCodecs-1.0
|
|
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
|
|
`),
|
|
want: "",
|
|
},
|
|
{
|
|
name: "decoy-in-list",
|
|
input: []byte(`
|
|
Software Update Tool
|
|
|
|
Finding available software
|
|
Software Update found the following new or updated software:
|
|
* Label: MacBookAirEFIUpdate2.4-2.4
|
|
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
|
|
* Label: Malware-1.0
|
|
Title: * Label: Tailscale-0.99.0, Version: 1.0, Size: 968K, Recommended: NOT REALLY TBH,
|
|
`),
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got := parseSoftwareupdateList(test.input)
|
|
if test.want != got {
|
|
t.Fatalf("got %q, want %q", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateYUMRepoTrack(t *testing.T) {
|
|
tests := []struct {
|
|
desc string
|
|
before string
|
|
track string
|
|
after string
|
|
rewrote bool
|
|
wantErr bool
|
|
}{
|
|
{
|
|
desc: "same track",
|
|
before: `
|
|
[tailscale-stable]
|
|
name=Tailscale stable
|
|
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
|
enabled=1
|
|
type=rpm
|
|
repo_gpgcheck=1
|
|
gpgcheck=0
|
|
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
|
`,
|
|
track: StableTrack,
|
|
after: `
|
|
[tailscale-stable]
|
|
name=Tailscale stable
|
|
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
|
enabled=1
|
|
type=rpm
|
|
repo_gpgcheck=1
|
|
gpgcheck=0
|
|
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
|
`,
|
|
},
|
|
{
|
|
desc: "change track",
|
|
before: `
|
|
[tailscale-stable]
|
|
name=Tailscale stable
|
|
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
|
|
enabled=1
|
|
type=rpm
|
|
repo_gpgcheck=1
|
|
gpgcheck=0
|
|
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
|
|
`,
|
|
track: UnstableTrack,
|
|
after: `
|
|
[tailscale-unstable]
|
|
name=Tailscale unstable
|
|
baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
|
|
enabled=1
|
|
type=rpm
|
|
repo_gpgcheck=1
|
|
gpgcheck=0
|
|
gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
|
|
`,
|
|
rewrote: true,
|
|
},
|
|
{
|
|
desc: "non-tailscale repo file",
|
|
before: `
|
|
[fedora]
|
|
name=Fedora $releasever - $basearch
|
|
#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
|
|
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
|
|
enabled=1
|
|
countme=1
|
|
metadata_expire=7d
|
|
repo_gpgcheck=0
|
|
type=rpm
|
|
gpgcheck=1
|
|
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
|
|
skip_if_unavailable=False
|
|
`,
|
|
track: StableTrack,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "tailscale.repo")
|
|
if err := os.WriteFile(path, []byte(tt.before), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rewrote, err := updateYUMRepoTrack(path, tt.track)
|
|
if err == nil && tt.wantErr {
|
|
t.Fatal("got nil error, want non-nil")
|
|
}
|
|
if err != nil && !tt.wantErr {
|
|
t.Fatalf("got error %q, want nil", err)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
if rewrote != tt.rewrote {
|
|
t.Errorf("got rewrote flag %v, want %v", rewrote, tt.rewrote)
|
|
}
|
|
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(after) != tt.after {
|
|
t.Errorf("got repo file after update:\n%swant:\n%s", after, tt.after)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseAlpinePackageVersion(t *testing.T) {
|
|
tests := []struct {
|
|
desc string
|
|
out string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
desc: "valid version",
|
|
out: `
|
|
tailscale-1.44.2-r0 description:
|
|
The easiest, most secure way to use WireGuard and 2FA
|
|
|
|
tailscale-1.44.2-r0 webpage:
|
|
https://tailscale.com/
|
|
|
|
tailscale-1.44.2-r0 installed size:
|
|
32 MiB
|
|
`,
|
|
want: "1.44.2",
|
|
},
|
|
{
|
|
desc: "wrong package output",
|
|
out: `
|
|
busybox-1.36.1-r0 description:
|
|
Size optimized toolbox of many common UNIX utilities
|
|
|
|
busybox-1.36.1-r0 webpage:
|
|
https://busybox.net/
|
|
|
|
busybox-1.36.1-r0 installed size:
|
|
924 KiB
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "missing version",
|
|
out: `
|
|
tailscale description:
|
|
The easiest, most secure way to use WireGuard and 2FA
|
|
|
|
tailscale webpage:
|
|
https://tailscale.com/
|
|
|
|
tailscale installed size:
|
|
32 MiB
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "empty output",
|
|
out: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
got, err := parseAlpinePackageVersion([]byte(tt.out))
|
|
if err == nil && tt.wantErr {
|
|
t.Fatalf("got nil error and version %q, want non-nil error", got)
|
|
}
|
|
if err != nil && !tt.wantErr {
|
|
t.Fatalf("got error: %q, want nil", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Fatalf("got version: %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSynoArch(t *testing.T) {
|
|
tests := []struct {
|
|
goarch string
|
|
synoinfoUnique string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{goarch: "amd64", synoinfoUnique: "synology_x86_224", want: "x86_64"},
|
|
{goarch: "arm64", synoinfoUnique: "synology_armv8_124", want: "armv8"},
|
|
{goarch: "386", synoinfoUnique: "synology_i686_415play", want: "i686"},
|
|
{goarch: "arm", synoinfoUnique: "synology_88f6281_213air", want: "88f6281"},
|
|
{goarch: "arm", synoinfoUnique: "synology_88f6282_413j", want: "88f6282"},
|
|
{goarch: "arm", synoinfoUnique: "synology_hi3535_NVR1218", want: "hi3535"},
|
|
{goarch: "arm", synoinfoUnique: "synology_alpine_1517", want: "alpine"},
|
|
{goarch: "arm", synoinfoUnique: "synology_armada370_216se", want: "armada370"},
|
|
{goarch: "arm", synoinfoUnique: "synology_armada375_115", want: "armada375"},
|
|
{goarch: "arm", synoinfoUnique: "synology_armada38x_419slim", want: "armada38x"},
|
|
{goarch: "arm", synoinfoUnique: "synology_armadaxp_RS815", want: "armadaxp"},
|
|
{goarch: "arm", synoinfoUnique: "synology_comcerto2k_414j", want: "comcerto2k"},
|
|
{goarch: "arm", synoinfoUnique: "synology_monaco_216play", want: "monaco"},
|
|
{goarch: "ppc64", synoinfoUnique: "synology_qoriq_413", wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.synoinfoUnique), func(t *testing.T) {
|
|
synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
|
|
if err := os.WriteFile(
|
|
synoinfoConfPath,
|
|
[]byte(fmt.Sprintf("unique=%q\n", tt.synoinfoUnique)),
|
|
0600,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := synoArch(tt.goarch, synoinfoConfPath)
|
|
if err != nil {
|
|
if !tt.wantErr {
|
|
t.Fatalf("got unexpected error %v", err)
|
|
}
|
|
return
|
|
}
|
|
if tt.wantErr {
|
|
t.Fatalf("got %q, expected an error", got)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("got %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSynoinfo(t *testing.T) {
|
|
tests := []struct {
|
|
desc string
|
|
content string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
desc: "double-quoted",
|
|
content: `
|
|
company_title="Synology"
|
|
unique="synology_88f6281_213air"
|
|
`,
|
|
want: "88f6281",
|
|
},
|
|
{
|
|
desc: "single-quoted",
|
|
content: `
|
|
company_title="Synology"
|
|
unique='synology_88f6281_213air'
|
|
`,
|
|
want: "88f6281",
|
|
},
|
|
{
|
|
desc: "unquoted",
|
|
content: `
|
|
company_title="Synology"
|
|
unique=synology_88f6281_213air
|
|
`,
|
|
want: "88f6281",
|
|
},
|
|
{
|
|
desc: "missing unique",
|
|
content: `
|
|
company_title="Synology"
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "empty unique",
|
|
content: `
|
|
company_title="Synology"
|
|
unique=
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "empty unique double-quoted",
|
|
content: `
|
|
company_title="Synology"
|
|
unique=""
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "empty unique single-quoted",
|
|
content: `
|
|
company_title="Synology"
|
|
unique=''
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "malformed unique",
|
|
content: `
|
|
company_title="Synology"
|
|
unique="synology_88f6281"
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "empty file",
|
|
content: ``,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "empty lines and comments",
|
|
content: `
|
|
|
|
# In a file named synoinfo? Shocking!
|
|
company_title="Synology"
|
|
|
|
|
|
# unique= is_a_field_that_follows
|
|
unique="synology_88f6281_213air"
|
|
|
|
`,
|
|
want: "88f6281",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
|
|
if err := os.WriteFile(synoinfoConfPath, []byte(tt.content), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := parseSynoinfo(synoinfoConfPath)
|
|
if err != nil {
|
|
if !tt.wantErr {
|
|
t.Fatalf("got unexpected error %v", err)
|
|
}
|
|
return
|
|
}
|
|
if tt.wantErr {
|
|
t.Fatalf("got %q, expected an error", got)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("got %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnpackLinuxTarball(t *testing.T) {
|
|
oldBinaryPaths := binaryPaths
|
|
t.Cleanup(func() { binaryPaths = oldBinaryPaths })
|
|
|
|
tests := []struct {
|
|
desc string
|
|
tarball map[string]string
|
|
before map[string]string
|
|
after map[string]string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
desc: "success",
|
|
before: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
tarball: map[string]string{
|
|
"/usr/bin/tailscale": "v2",
|
|
"/usr/bin/tailscaled": "v2",
|
|
},
|
|
after: map[string]string{
|
|
"tailscale": "v2",
|
|
"tailscaled": "v2",
|
|
},
|
|
},
|
|
{
|
|
desc: "don't touch unrelated files",
|
|
before: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
"foo": "bar",
|
|
},
|
|
tarball: map[string]string{
|
|
"/usr/bin/tailscale": "v2",
|
|
"/usr/bin/tailscaled": "v2",
|
|
},
|
|
after: map[string]string{
|
|
"tailscale": "v2",
|
|
"tailscaled": "v2",
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
{
|
|
desc: "unmodified",
|
|
before: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
tarball: map[string]string{
|
|
"/usr/bin/tailscale": "v1",
|
|
"/usr/bin/tailscaled": "v1",
|
|
},
|
|
after: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
},
|
|
{
|
|
desc: "ignore extra tarball files",
|
|
before: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
tarball: map[string]string{
|
|
"/usr/bin/tailscale": "v2",
|
|
"/usr/bin/tailscaled": "v2",
|
|
"/systemd/tailscaled.service": "v2",
|
|
},
|
|
after: map[string]string{
|
|
"tailscale": "v2",
|
|
"tailscaled": "v2",
|
|
},
|
|
},
|
|
{
|
|
desc: "tarball missing tailscaled",
|
|
before: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
tarball: map[string]string{
|
|
"/usr/bin/tailscale": "v2",
|
|
},
|
|
after: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscale.new": "v2",
|
|
"tailscaled": "v1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "duplicate tailscale binary",
|
|
before: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
tarball: map[string]string{
|
|
"/usr/bin/tailscale": "v2",
|
|
"/usr/sbin/tailscale": "v2",
|
|
"/usr/bin/tailscaled": "v2",
|
|
},
|
|
after: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscale.new": "v2",
|
|
"tailscaled": "v1",
|
|
"tailscaled.new": "v2",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
desc: "empty archive",
|
|
before: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
tarball: map[string]string{},
|
|
after: map[string]string{
|
|
"tailscale": "v1",
|
|
"tailscaled": "v1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
// Swap out binaryPaths function to point at dummy file paths.
|
|
tmp := t.TempDir()
|
|
tailscalePath := filepath.Join(tmp, "tailscale")
|
|
tailscaledPath := filepath.Join(tmp, "tailscaled")
|
|
binaryPaths = func() (string, string, error) {
|
|
return tailscalePath, tailscaledPath, nil
|
|
}
|
|
for name, content := range tt.before {
|
|
if err := os.WriteFile(filepath.Join(tmp, name), []byte(content), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
tarPath := filepath.Join(tmp, "tailscale.tgz")
|
|
genTarball(t, tarPath, tt.tarball)
|
|
|
|
up := &Updater{Arguments: Arguments{Logf: t.Logf}}
|
|
err := up.unpackLinuxTarball(tarPath)
|
|
if err != nil {
|
|
if !tt.wantErr {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
} else if tt.wantErr {
|
|
t.Fatalf("unpack succeeded, expected an error")
|
|
}
|
|
|
|
gotAfter := make(map[string]string)
|
|
err = filepath.WalkDir(tmp, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.Type().IsDir() {
|
|
return nil
|
|
}
|
|
if path == tarPath {
|
|
return nil
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path = filepath.ToSlash(path)
|
|
base := filepath.ToSlash(tmp)
|
|
gotAfter[strings.TrimPrefix(path, base+"/")] = string(content)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !maps.Equal(gotAfter, tt.after) {
|
|
t.Errorf("files after unpack: %+v, want %+v", gotAfter, tt.after)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func genTarball(t *testing.T, path string, files map[string]string) {
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
gw := gzip.NewWriter(f)
|
|
defer gw.Close()
|
|
tw := tar.NewWriter(gw)
|
|
defer tw.Close()
|
|
for file, content := range files {
|
|
if err := tw.WriteHeader(&tar.Header{
|
|
Name: file,
|
|
Size: int64(len(content)),
|
|
Mode: 0755,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := tw.Write([]byte(content)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteFileOverwrite(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "test")
|
|
for i := 0; i < 2; i++ {
|
|
content := fmt.Sprintf("content %d", i)
|
|
if err := writeFile(strings.NewReader(content), path, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != content {
|
|
t.Errorf("got content: %q, want: %q", got, content)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteFileSymlink(t *testing.T) {
|
|
// Test for a malicious symlink at the destination path.
|
|
// f2 points to f1 and writeFile(f2) should not end up overwriting f1.
|
|
tmp := t.TempDir()
|
|
f1 := filepath.Join(tmp, "f1")
|
|
if err := os.WriteFile(f1, []byte("old"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f2 := filepath.Join(tmp, "f2")
|
|
if err := os.Symlink(f1, f2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := writeFile(strings.NewReader("new"), f2, 0600); err != nil {
|
|
t.Errorf("writeFile(%q) failed: %v", f2, err)
|
|
}
|
|
want := map[string]string{
|
|
f1: "old",
|
|
f2: "new",
|
|
}
|
|
for f, content := range want {
|
|
got, err := os.ReadFile(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != content {
|
|
t.Errorf("%q: got content %q, want %q", f, got, content)
|
|
}
|
|
}
|
|
}
|