clientupdate_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package clientupdate
  4. import (
  5. "archive/tar"
  6. "compress/gzip"
  7. "fmt"
  8. "io/fs"
  9. "maps"
  10. "os"
  11. "path/filepath"
  12. "strings"
  13. "testing"
  14. )
  15. func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
  16. tests := []struct {
  17. name string
  18. toTrack string
  19. in string
  20. want string // empty means want no change
  21. wantErr string
  22. }{
  23. {
  24. name: "stable-to-unstable",
  25. toTrack: UnstableTrack,
  26. in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
  27. want: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
  28. },
  29. {
  30. name: "stable-unchanged",
  31. toTrack: StableTrack,
  32. in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
  33. },
  34. {
  35. name: "if-both-stable-and-unstable-dont-change",
  36. toTrack: StableTrack,
  37. in: "# Tailscale packages for debian buster\n" +
  38. "deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
  39. "deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
  40. },
  41. {
  42. name: "if-both-stable-and-unstable-dont-change-unstable",
  43. toTrack: UnstableTrack,
  44. in: "# Tailscale packages for debian buster\n" +
  45. "deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
  46. "deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
  47. },
  48. {
  49. name: "signed-by-form",
  50. toTrack: UnstableTrack,
  51. 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",
  52. 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",
  53. },
  54. {
  55. name: "unsupported-lines",
  56. toTrack: UnstableTrack,
  57. 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",
  58. wantErr: "unexpected/unsupported /etc/apt/sources.list.d/tailscale.list contents",
  59. },
  60. }
  61. for _, tt := range tests {
  62. t.Run(tt.name, func(t *testing.T) {
  63. newContent, err := updateDebianAptSourcesListBytes([]byte(tt.in), tt.toTrack)
  64. if err != nil {
  65. if err.Error() != tt.wantErr {
  66. t.Fatalf("error = %v; want %q", err, tt.wantErr)
  67. }
  68. return
  69. }
  70. if tt.wantErr != "" {
  71. t.Fatalf("got no error; want %q", tt.wantErr)
  72. }
  73. var gotChange string
  74. if string(newContent) != tt.in {
  75. gotChange = string(newContent)
  76. }
  77. if gotChange != tt.want {
  78. t.Errorf("wrong result\n got: %q\nwant: %q", gotChange, tt.want)
  79. }
  80. })
  81. }
  82. }
  83. func TestParseSoftwareupdateList(t *testing.T) {
  84. tests := []struct {
  85. name string
  86. input []byte
  87. want string
  88. }{
  89. {
  90. name: "update-at-end-of-list",
  91. input: []byte(`
  92. Software Update Tool
  93. Finding available software
  94. Software Update found the following new or updated software:
  95. * Label: MacBookAirEFIUpdate2.4-2.4
  96. Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
  97. * Label: ProAppsQTCodecs-1.0
  98. Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
  99. * Label: Tailscale-1.23.4
  100. Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
  101. `),
  102. want: "Tailscale-1.23.4",
  103. },
  104. {
  105. name: "update-in-middle-of-list",
  106. input: []byte(`
  107. Software Update Tool
  108. Finding available software
  109. Software Update found the following new or updated software:
  110. * Label: MacBookAirEFIUpdate2.4-2.4
  111. Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
  112. * Label: Tailscale-1.23.5000
  113. Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
  114. * Label: ProAppsQTCodecs-1.0
  115. Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
  116. `),
  117. want: "Tailscale-1.23.5000",
  118. },
  119. {
  120. name: "update-not-in-list",
  121. input: []byte(`
  122. Software Update Tool
  123. Finding available software
  124. Software Update found the following new or updated software:
  125. * Label: MacBookAirEFIUpdate2.4-2.4
  126. Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
  127. * Label: ProAppsQTCodecs-1.0
  128. Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
  129. `),
  130. want: "",
  131. },
  132. {
  133. name: "decoy-in-list",
  134. input: []byte(`
  135. Software Update Tool
  136. Finding available software
  137. Software Update found the following new or updated software:
  138. * Label: MacBookAirEFIUpdate2.4-2.4
  139. Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
  140. * Label: Malware-1.0
  141. Title: * Label: Tailscale-0.99.0, Version: 1.0, Size: 968K, Recommended: NOT REALLY TBH,
  142. `),
  143. want: "",
  144. },
  145. }
  146. for _, test := range tests {
  147. t.Run(test.name, func(t *testing.T) {
  148. got := parseSoftwareupdateList(test.input)
  149. if test.want != got {
  150. t.Fatalf("got %q, want %q", got, test.want)
  151. }
  152. })
  153. }
  154. }
  155. func TestUpdateYUMRepoTrack(t *testing.T) {
  156. tests := []struct {
  157. desc string
  158. before string
  159. track string
  160. after string
  161. rewrote bool
  162. wantErr bool
  163. }{
  164. {
  165. desc: "same track",
  166. before: `
  167. [tailscale-stable]
  168. name=Tailscale stable
  169. baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
  170. enabled=1
  171. type=rpm
  172. repo_gpgcheck=1
  173. gpgcheck=0
  174. gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
  175. `,
  176. track: StableTrack,
  177. after: `
  178. [tailscale-stable]
  179. name=Tailscale stable
  180. baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
  181. enabled=1
  182. type=rpm
  183. repo_gpgcheck=1
  184. gpgcheck=0
  185. gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
  186. `,
  187. },
  188. {
  189. desc: "change track",
  190. before: `
  191. [tailscale-stable]
  192. name=Tailscale stable
  193. baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
  194. enabled=1
  195. type=rpm
  196. repo_gpgcheck=1
  197. gpgcheck=0
  198. gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
  199. `,
  200. track: UnstableTrack,
  201. after: `
  202. [tailscale-unstable]
  203. name=Tailscale unstable
  204. baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
  205. enabled=1
  206. type=rpm
  207. repo_gpgcheck=1
  208. gpgcheck=0
  209. gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
  210. `,
  211. rewrote: true,
  212. },
  213. {
  214. desc: "non-tailscale repo file",
  215. before: `
  216. [fedora]
  217. name=Fedora $releasever - $basearch
  218. #baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
  219. metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
  220. enabled=1
  221. countme=1
  222. metadata_expire=7d
  223. repo_gpgcheck=0
  224. type=rpm
  225. gpgcheck=1
  226. gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
  227. skip_if_unavailable=False
  228. `,
  229. track: StableTrack,
  230. wantErr: true,
  231. },
  232. }
  233. for _, tt := range tests {
  234. t.Run(tt.desc, func(t *testing.T) {
  235. path := filepath.Join(t.TempDir(), "tailscale.repo")
  236. if err := os.WriteFile(path, []byte(tt.before), 0644); err != nil {
  237. t.Fatal(err)
  238. }
  239. rewrote, err := updateYUMRepoTrack(path, tt.track)
  240. if err == nil && tt.wantErr {
  241. t.Fatal("got nil error, want non-nil")
  242. }
  243. if err != nil && !tt.wantErr {
  244. t.Fatalf("got error %q, want nil", err)
  245. }
  246. if err != nil {
  247. return
  248. }
  249. if rewrote != tt.rewrote {
  250. t.Errorf("got rewrote flag %v, want %v", rewrote, tt.rewrote)
  251. }
  252. after, err := os.ReadFile(path)
  253. if err != nil {
  254. t.Fatal(err)
  255. }
  256. if string(after) != tt.after {
  257. t.Errorf("got repo file after update:\n%swant:\n%s", after, tt.after)
  258. }
  259. })
  260. }
  261. }
  262. func TestParseAlpinePackageVersion(t *testing.T) {
  263. tests := []struct {
  264. desc string
  265. out string
  266. want string
  267. wantErr bool
  268. }{
  269. {
  270. desc: "valid version",
  271. out: `
  272. tailscale-1.44.2-r0 description:
  273. The easiest, most secure way to use WireGuard and 2FA
  274. tailscale-1.44.2-r0 webpage:
  275. https://tailscale.com/
  276. tailscale-1.44.2-r0 installed size:
  277. 32 MiB
  278. `,
  279. want: "1.44.2",
  280. },
  281. {
  282. desc: "wrong package output",
  283. out: `
  284. busybox-1.36.1-r0 description:
  285. Size optimized toolbox of many common UNIX utilities
  286. busybox-1.36.1-r0 webpage:
  287. https://busybox.net/
  288. busybox-1.36.1-r0 installed size:
  289. 924 KiB
  290. `,
  291. wantErr: true,
  292. },
  293. {
  294. desc: "missing version",
  295. out: `
  296. tailscale description:
  297. The easiest, most secure way to use WireGuard and 2FA
  298. tailscale webpage:
  299. https://tailscale.com/
  300. tailscale installed size:
  301. 32 MiB
  302. `,
  303. wantErr: true,
  304. },
  305. {
  306. desc: "empty output",
  307. out: "",
  308. wantErr: true,
  309. },
  310. }
  311. for _, tt := range tests {
  312. t.Run(tt.desc, func(t *testing.T) {
  313. got, err := parseAlpinePackageVersion([]byte(tt.out))
  314. if err == nil && tt.wantErr {
  315. t.Fatalf("got nil error and version %q, want non-nil error", got)
  316. }
  317. if err != nil && !tt.wantErr {
  318. t.Fatalf("got error: %q, want nil", err)
  319. }
  320. if got != tt.want {
  321. t.Fatalf("got version: %q, want %q", got, tt.want)
  322. }
  323. })
  324. }
  325. }
  326. func TestSynoArch(t *testing.T) {
  327. tests := []struct {
  328. goarch string
  329. synoinfoUnique string
  330. want string
  331. wantErr bool
  332. }{
  333. {goarch: "amd64", synoinfoUnique: "synology_x86_224", want: "x86_64"},
  334. {goarch: "arm64", synoinfoUnique: "synology_armv8_124", want: "armv8"},
  335. {goarch: "386", synoinfoUnique: "synology_i686_415play", want: "i686"},
  336. {goarch: "arm", synoinfoUnique: "synology_88f6281_213air", want: "88f6281"},
  337. {goarch: "arm", synoinfoUnique: "synology_88f6282_413j", want: "88f6282"},
  338. {goarch: "arm", synoinfoUnique: "synology_hi3535_NVR1218", want: "hi3535"},
  339. {goarch: "arm", synoinfoUnique: "synology_alpine_1517", want: "alpine"},
  340. {goarch: "arm", synoinfoUnique: "synology_armada370_216se", want: "armada370"},
  341. {goarch: "arm", synoinfoUnique: "synology_armada375_115", want: "armada375"},
  342. {goarch: "arm", synoinfoUnique: "synology_armada38x_419slim", want: "armada38x"},
  343. {goarch: "arm", synoinfoUnique: "synology_armadaxp_RS815", want: "armadaxp"},
  344. {goarch: "arm", synoinfoUnique: "synology_comcerto2k_414j", want: "comcerto2k"},
  345. {goarch: "arm", synoinfoUnique: "synology_monaco_216play", want: "monaco"},
  346. {goarch: "ppc64", synoinfoUnique: "synology_qoriq_413", wantErr: true},
  347. }
  348. for _, tt := range tests {
  349. t.Run(fmt.Sprintf("%s-%s", tt.goarch, tt.synoinfoUnique), func(t *testing.T) {
  350. synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
  351. if err := os.WriteFile(
  352. synoinfoConfPath,
  353. []byte(fmt.Sprintf("unique=%q\n", tt.synoinfoUnique)),
  354. 0600,
  355. ); err != nil {
  356. t.Fatal(err)
  357. }
  358. got, err := synoArch(tt.goarch, synoinfoConfPath)
  359. if err != nil {
  360. if !tt.wantErr {
  361. t.Fatalf("got unexpected error %v", err)
  362. }
  363. return
  364. }
  365. if tt.wantErr {
  366. t.Fatalf("got %q, expected an error", got)
  367. }
  368. if got != tt.want {
  369. t.Errorf("got %q, want %q", got, tt.want)
  370. }
  371. })
  372. }
  373. }
  374. func TestParseSynoinfo(t *testing.T) {
  375. tests := []struct {
  376. desc string
  377. content string
  378. want string
  379. wantErr bool
  380. }{
  381. {
  382. desc: "double-quoted",
  383. content: `
  384. company_title="Synology"
  385. unique="synology_88f6281_213air"
  386. `,
  387. want: "88f6281",
  388. },
  389. {
  390. desc: "single-quoted",
  391. content: `
  392. company_title="Synology"
  393. unique='synology_88f6281_213air'
  394. `,
  395. want: "88f6281",
  396. },
  397. {
  398. desc: "unquoted",
  399. content: `
  400. company_title="Synology"
  401. unique=synology_88f6281_213air
  402. `,
  403. want: "88f6281",
  404. },
  405. {
  406. desc: "missing unique",
  407. content: `
  408. company_title="Synology"
  409. `,
  410. wantErr: true,
  411. },
  412. {
  413. desc: "empty unique",
  414. content: `
  415. company_title="Synology"
  416. unique=
  417. `,
  418. wantErr: true,
  419. },
  420. {
  421. desc: "empty unique double-quoted",
  422. content: `
  423. company_title="Synology"
  424. unique=""
  425. `,
  426. wantErr: true,
  427. },
  428. {
  429. desc: "empty unique single-quoted",
  430. content: `
  431. company_title="Synology"
  432. unique=''
  433. `,
  434. wantErr: true,
  435. },
  436. {
  437. desc: "malformed unique",
  438. content: `
  439. company_title="Synology"
  440. unique="synology_88f6281"
  441. `,
  442. wantErr: true,
  443. },
  444. {
  445. desc: "empty file",
  446. content: ``,
  447. wantErr: true,
  448. },
  449. {
  450. desc: "empty lines and comments",
  451. content: `
  452. # In a file named synoinfo? Shocking!
  453. company_title="Synology"
  454. # unique= is_a_field_that_follows
  455. unique="synology_88f6281_213air"
  456. `,
  457. want: "88f6281",
  458. },
  459. }
  460. for _, tt := range tests {
  461. t.Run(tt.desc, func(t *testing.T) {
  462. synoinfoConfPath := filepath.Join(t.TempDir(), "synoinfo.conf")
  463. if err := os.WriteFile(synoinfoConfPath, []byte(tt.content), 0600); err != nil {
  464. t.Fatal(err)
  465. }
  466. got, err := parseSynoinfo(synoinfoConfPath)
  467. if err != nil {
  468. if !tt.wantErr {
  469. t.Fatalf("got unexpected error %v", err)
  470. }
  471. return
  472. }
  473. if tt.wantErr {
  474. t.Fatalf("got %q, expected an error", got)
  475. }
  476. if got != tt.want {
  477. t.Errorf("got %q, want %q", got, tt.want)
  478. }
  479. })
  480. }
  481. }
  482. func TestUnpackLinuxTarball(t *testing.T) {
  483. oldBinaryPaths := binaryPaths
  484. t.Cleanup(func() { binaryPaths = oldBinaryPaths })
  485. tests := []struct {
  486. desc string
  487. tarball map[string]string
  488. before map[string]string
  489. after map[string]string
  490. wantErr bool
  491. }{
  492. {
  493. desc: "success",
  494. before: map[string]string{
  495. "tailscale": "v1",
  496. "tailscaled": "v1",
  497. },
  498. tarball: map[string]string{
  499. "/usr/bin/tailscale": "v2",
  500. "/usr/bin/tailscaled": "v2",
  501. },
  502. after: map[string]string{
  503. "tailscale": "v2",
  504. "tailscaled": "v2",
  505. },
  506. },
  507. {
  508. desc: "don't touch unrelated files",
  509. before: map[string]string{
  510. "tailscale": "v1",
  511. "tailscaled": "v1",
  512. "foo": "bar",
  513. },
  514. tarball: map[string]string{
  515. "/usr/bin/tailscale": "v2",
  516. "/usr/bin/tailscaled": "v2",
  517. },
  518. after: map[string]string{
  519. "tailscale": "v2",
  520. "tailscaled": "v2",
  521. "foo": "bar",
  522. },
  523. },
  524. {
  525. desc: "unmodified",
  526. before: map[string]string{
  527. "tailscale": "v1",
  528. "tailscaled": "v1",
  529. },
  530. tarball: map[string]string{
  531. "/usr/bin/tailscale": "v1",
  532. "/usr/bin/tailscaled": "v1",
  533. },
  534. after: map[string]string{
  535. "tailscale": "v1",
  536. "tailscaled": "v1",
  537. },
  538. },
  539. {
  540. desc: "ignore extra tarball files",
  541. before: map[string]string{
  542. "tailscale": "v1",
  543. "tailscaled": "v1",
  544. },
  545. tarball: map[string]string{
  546. "/usr/bin/tailscale": "v2",
  547. "/usr/bin/tailscaled": "v2",
  548. "/systemd/tailscaled.service": "v2",
  549. },
  550. after: map[string]string{
  551. "tailscale": "v2",
  552. "tailscaled": "v2",
  553. },
  554. },
  555. {
  556. desc: "tarball missing tailscaled",
  557. before: map[string]string{
  558. "tailscale": "v1",
  559. "tailscaled": "v1",
  560. },
  561. tarball: map[string]string{
  562. "/usr/bin/tailscale": "v2",
  563. },
  564. after: map[string]string{
  565. "tailscale": "v1",
  566. "tailscale.new": "v2",
  567. "tailscaled": "v1",
  568. },
  569. wantErr: true,
  570. },
  571. {
  572. desc: "duplicate tailscale binary",
  573. before: map[string]string{
  574. "tailscale": "v1",
  575. "tailscaled": "v1",
  576. },
  577. tarball: map[string]string{
  578. "/usr/bin/tailscale": "v2",
  579. "/usr/sbin/tailscale": "v2",
  580. "/usr/bin/tailscaled": "v2",
  581. },
  582. after: map[string]string{
  583. "tailscale": "v1",
  584. "tailscale.new": "v2",
  585. "tailscaled": "v1",
  586. "tailscaled.new": "v2",
  587. },
  588. wantErr: true,
  589. },
  590. {
  591. desc: "empty archive",
  592. before: map[string]string{
  593. "tailscale": "v1",
  594. "tailscaled": "v1",
  595. },
  596. tarball: map[string]string{},
  597. after: map[string]string{
  598. "tailscale": "v1",
  599. "tailscaled": "v1",
  600. },
  601. wantErr: true,
  602. },
  603. }
  604. for _, tt := range tests {
  605. t.Run(tt.desc, func(t *testing.T) {
  606. // Swap out binaryPaths function to point at dummy file paths.
  607. tmp := t.TempDir()
  608. tailscalePath := filepath.Join(tmp, "tailscale")
  609. tailscaledPath := filepath.Join(tmp, "tailscaled")
  610. binaryPaths = func() (string, string, error) {
  611. return tailscalePath, tailscaledPath, nil
  612. }
  613. for name, content := range tt.before {
  614. if err := os.WriteFile(filepath.Join(tmp, name), []byte(content), 0755); err != nil {
  615. t.Fatal(err)
  616. }
  617. }
  618. tarPath := filepath.Join(tmp, "tailscale.tgz")
  619. genTarball(t, tarPath, tt.tarball)
  620. up := &Updater{Arguments: Arguments{Logf: t.Logf}}
  621. err := up.unpackLinuxTarball(tarPath)
  622. if err != nil {
  623. if !tt.wantErr {
  624. t.Fatalf("unexpected error: %v", err)
  625. }
  626. } else if tt.wantErr {
  627. t.Fatalf("unpack succeeded, expected an error")
  628. }
  629. gotAfter := make(map[string]string)
  630. err = filepath.WalkDir(tmp, func(path string, d fs.DirEntry, err error) error {
  631. if err != nil {
  632. return err
  633. }
  634. if d.Type().IsDir() {
  635. return nil
  636. }
  637. if path == tarPath {
  638. return nil
  639. }
  640. content, err := os.ReadFile(path)
  641. if err != nil {
  642. return err
  643. }
  644. path = filepath.ToSlash(path)
  645. base := filepath.ToSlash(tmp)
  646. gotAfter[strings.TrimPrefix(path, base+"/")] = string(content)
  647. return nil
  648. })
  649. if err != nil {
  650. t.Fatal(err)
  651. }
  652. if !maps.Equal(gotAfter, tt.after) {
  653. t.Errorf("files after unpack: %+v, want %+v", gotAfter, tt.after)
  654. }
  655. })
  656. }
  657. }
  658. func genTarball(t *testing.T, path string, files map[string]string) {
  659. f, err := os.Create(path)
  660. if err != nil {
  661. t.Fatal(err)
  662. }
  663. defer f.Close()
  664. gw := gzip.NewWriter(f)
  665. defer gw.Close()
  666. tw := tar.NewWriter(gw)
  667. defer tw.Close()
  668. for file, content := range files {
  669. if err := tw.WriteHeader(&tar.Header{
  670. Name: file,
  671. Size: int64(len(content)),
  672. Mode: 0755,
  673. }); err != nil {
  674. t.Fatal(err)
  675. }
  676. if _, err := tw.Write([]byte(content)); err != nil {
  677. t.Fatal(err)
  678. }
  679. }
  680. }
  681. func TestWriteFileOverwrite(t *testing.T) {
  682. path := filepath.Join(t.TempDir(), "test")
  683. for i := 0; i < 2; i++ {
  684. content := fmt.Sprintf("content %d", i)
  685. if err := writeFile(strings.NewReader(content), path, 0600); err != nil {
  686. t.Fatal(err)
  687. }
  688. got, err := os.ReadFile(path)
  689. if err != nil {
  690. t.Fatal(err)
  691. }
  692. if string(got) != content {
  693. t.Errorf("got content: %q, want: %q", got, content)
  694. }
  695. }
  696. }
  697. func TestWriteFileSymlink(t *testing.T) {
  698. // Test for a malicious symlink at the destination path.
  699. // f2 points to f1 and writeFile(f2) should not end up overwriting f1.
  700. tmp := t.TempDir()
  701. f1 := filepath.Join(tmp, "f1")
  702. if err := os.WriteFile(f1, []byte("old"), 0600); err != nil {
  703. t.Fatal(err)
  704. }
  705. f2 := filepath.Join(tmp, "f2")
  706. if err := os.Symlink(f1, f2); err != nil {
  707. t.Fatal(err)
  708. }
  709. if err := writeFile(strings.NewReader("new"), f2, 0600); err != nil {
  710. t.Errorf("writeFile(%q) failed: %v", f2, err)
  711. }
  712. want := map[string]string{
  713. f1: "old",
  714. f2: "new",
  715. }
  716. for f, content := range want {
  717. got, err := os.ReadFile(f)
  718. if err != nil {
  719. t.Fatal(err)
  720. }
  721. if string(got) != content {
  722. t.Errorf("%q: got content %q, want %q", f, got, content)
  723. }
  724. }
  725. }