Merge pull request #190 from bnema/fix/podman-backup-copy-basename
Fix backup extraction with basename tar entries
2 files · +145 -17
M internal/adapters/out/docker/runtime.go +80 -12
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | 11 | "fmt" | |
| 12 | 12 | "io" | |
| 13 | 13 | "math" | |
| 14 | + | "os" | |
| 14 | 15 | "path/filepath" | |
| 15 | 16 | "sort" | |
| 16 | 17 | "strconv" | |
| @@ -49,6 +50,11 @@ type pipeReadCloser struct { | |||
| 49 | 50 | once sync.Once | |
| 50 | 51 | } | |
| 51 | 52 | ||
| 53 | + | type removeOnCloseFile struct { | |
| 54 | + | *os.File | |
| 55 | + | path string | |
| 56 | + | } | |
| 57 | + | ||
| 52 | 58 | func (p *pipeReadCloser) Read(b []byte) (int, error) { | |
| 53 | 59 | return p.pr.Read(b) | |
| 54 | 60 | } | |
| @@ -71,6 +77,15 @@ func (p *pipeReadCloser) Close() error { | |||
| 71 | 77 | return closeErr | |
| 72 | 78 | } | |
| 73 | 79 | ||
| 80 | + | func (f *removeOnCloseFile) Close() error { | |
| 81 | + | closeErr := f.File.Close() | |
| 82 | + | removeErr := os.Remove(f.path) | |
| 83 | + | if closeErr != nil { | |
| 84 | + | return closeErr | |
| 85 | + | } | |
| 86 | + | return removeErr | |
| 87 | + | } | |
| 88 | + | ||
| 74 | 89 | // NewRuntime creates a new Docker runtime instance. | |
| 75 | 90 | func NewRuntime() (*Runtime, error) { | |
| 76 | 91 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | |
| @@ -1505,6 +1520,9 @@ func (r *Runtime) CopyFromContainer(ctx context.Context, containerID, srcPath st | |||
| 1505 | 1520 | func extractFileFromTar(reader io.ReadCloser, targetPath string) (io.ReadCloser, error) { | |
| 1506 | 1521 | tr := tar.NewReader(reader) | |
| 1507 | 1522 | targetName := normalizedTarPath(targetPath) | |
| 1523 | + | targetBase := filepath.Base(targetName) | |
| 1524 | + | var fallback *removeOnCloseFile | |
| 1525 | + | fallbackMatches := 0 | |
| 1508 | 1526 | ||
| 1509 | 1527 | for { | |
| 1510 | 1528 | header, err := tr.Next() | |
| @@ -1512,29 +1530,79 @@ func extractFileFromTar(reader io.ReadCloser, targetPath string) (io.ReadCloser, | |||
| 1512 | 1530 | break | |
| 1513 | 1531 | } | |
| 1514 | 1532 | if err != nil { | |
| 1533 | + | cleanupFallback(fallback) | |
| 1534 | + | _ = reader.Close() | |
| 1515 | 1535 | return nil, err | |
| 1516 | 1536 | } | |
| 1537 | + | if header.Typeflag != tar.TypeReg { | |
| 1538 | + | continue | |
| 1539 | + | } | |
| 1517 | 1540 | ||
| 1518 | - | if header.Typeflag == tar.TypeReg && normalizedTarPath(header.Name) == targetName { | |
| 1519 | - | size := header.Size | |
| 1520 | - | pr, pw := io.Pipe() | |
| 1521 | - | go func() { | |
| 1522 | - | defer reader.Close() | |
| 1523 | - | _, copyErr := io.CopyN(pw, tr, size) | |
| 1524 | - | if copyErr != nil { | |
| 1525 | - | _ = pw.CloseWithError(copyErr) | |
| 1526 | - | return | |
| 1541 | + | headerName := normalizedTarPath(header.Name) | |
| 1542 | + | if headerName == targetName { | |
| 1543 | + | cleanupFallback(fallback) | |
| 1544 | + | return streamCurrentTarFile(reader, tr, header.Size), nil | |
| 1545 | + | } | |
| 1546 | + | if headerName == targetBase { | |
| 1547 | + | fallbackMatches++ | |
| 1548 | + | if fallbackMatches == 1 { | |
| 1549 | + | fallback, err = bufferCurrentTarFile(tr, header.Size) | |
| 1550 | + | if err != nil { | |
| 1551 | + | _ = reader.Close() | |
| 1552 | + | return nil, err | |
| 1527 | 1553 | } | |
| 1528 | - | _ = pw.Close() | |
| 1529 | - | }() | |
| 1530 | - | return &pipeReadCloser{pr: pr, pw: pw, original: reader}, nil | |
| 1554 | + | } | |
| 1531 | 1555 | } | |
| 1532 | 1556 | } | |
| 1533 | 1557 | ||
| 1534 | 1558 | _ = reader.Close() | |
| 1559 | + | if fallbackMatches == 1 { | |
| 1560 | + | if _, err := fallback.Seek(0, io.SeekStart); err != nil { | |
| 1561 | + | cleanupFallback(fallback) | |
| 1562 | + | return nil, err | |
| 1563 | + | } | |
| 1564 | + | return fallback, nil | |
| 1565 | + | } | |
| 1566 | + | cleanupFallback(fallback) | |
| 1567 | + | if fallbackMatches > 1 { | |
| 1568 | + | return nil, fmt.Errorf("ambiguous basename matches in container archive for: %s", targetPath) | |
| 1569 | + | } | |
| 1535 | 1570 | return nil, fmt.Errorf("file not found in container: %s", targetPath) | |
| 1536 | 1571 | } | |
| 1537 | 1572 | ||
| 1573 | + | func streamCurrentTarFile(reader io.ReadCloser, tr *tar.Reader, size int64) io.ReadCloser { | |
| 1574 | + | pr, pw := io.Pipe() | |
| 1575 | + | go func() { | |
| 1576 | + | defer reader.Close() | |
| 1577 | + | _, copyErr := io.CopyN(pw, tr, size) | |
| 1578 | + | if copyErr != nil { | |
| 1579 | + | _ = pw.CloseWithError(copyErr) | |
| 1580 | + | return | |
| 1581 | + | } | |
| 1582 | + | _ = pw.Close() | |
| 1583 | + | }() | |
| 1584 | + | return &pipeReadCloser{pr: pr, pw: pw, original: reader} | |
| 1585 | + | } | |
| 1586 | + | ||
| 1587 | + | func bufferCurrentTarFile(tr *tar.Reader, size int64) (*removeOnCloseFile, error) { | |
| 1588 | + | file, err := os.CreateTemp("", "gordon-copy-from-container-*") | |
| 1589 | + | if err != nil { | |
| 1590 | + | return nil, err | |
| 1591 | + | } | |
| 1592 | + | fallback := &removeOnCloseFile{File: file, path: file.Name()} | |
| 1593 | + | if _, err := io.CopyN(fallback, tr, size); err != nil { | |
| 1594 | + | cleanupFallback(fallback) | |
| 1595 | + | return nil, err | |
| 1596 | + | } | |
| 1597 | + | return fallback, nil | |
| 1598 | + | } | |
| 1599 | + | ||
| 1600 | + | func cleanupFallback(fallback *removeOnCloseFile) { | |
| 1601 | + | if fallback != nil { | |
| 1602 | + | _ = fallback.Close() | |
| 1603 | + | } | |
| 1604 | + | } | |
| 1605 | + | ||
| 1538 | 1606 | func normalizedTarPath(path string) string { | |
| 1539 | 1607 | path = filepath.ToSlash(filepath.Clean(path)) | |
| 1540 | 1608 | return strings.TrimPrefix(path, "/") | |
| @@ -11,6 +11,7 @@ import ( | |||||
| 11 | "fmt" | 11 | "fmt" | ||
| 12 | "io" | 12 | "io" | ||
| 13 | "math" | 13 | "math" | ||
| 14 | + | "os" | |||
| 14 | "path/filepath" | 15 | "path/filepath" | ||
| 15 | "sort" | 16 | "sort" | ||
| 16 | "strconv" | 17 | "strconv" | ||
| @@ -49,6 +50,11 @@ type pipeReadCloser struct { | |||||
| 49 | once sync.Once | 50 | once sync.Once | ||
| 50 | } | 51 | } | ||
| 51 | 52 | ||||
| 53 | + | type removeOnCloseFile struct { | |||
| 54 | + | *os.File | |||
| 55 | + | path string | |||
| 56 | + | } | |||
| 57 | + | ||||
| 52 | func (p *pipeReadCloser) Read(b []byte) (int, error) { | 58 | func (p *pipeReadCloser) Read(b []byte) (int, error) { | ||
| 53 | return p.pr.Read(b) | 59 | return p.pr.Read(b) | ||
| 54 | } | 60 | } | ||
| @@ -71,6 +77,15 @@ func (p *pipeReadCloser) Close() error { | |||||
| 71 | return closeErr | 77 | return closeErr | ||
| 72 | } | 78 | } | ||
| 73 | 79 | ||||
| 80 | + | func (f *removeOnCloseFile) Close() error { | |||
| 81 | + | closeErr := f.File.Close() | |||
| 82 | + | removeErr := os.Remove(f.path) | |||
| 83 | + | if closeErr != nil { | |||
| 84 | + | return closeErr | |||
| 85 | + | } | |||
| 86 | + | return removeErr | |||
| 87 | + | } | |||
| 88 | + | ||||
| 74 | // NewRuntime creates a new Docker runtime instance. | 89 | // NewRuntime creates a new Docker runtime instance. | ||
| 75 | func NewRuntime() (*Runtime, error) { | 90 | func NewRuntime() (*Runtime, error) { | ||
| 76 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | 91 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | ||
| @@ -1505,6 +1520,9 @@ func (r *Runtime) CopyFromContainer(ctx context.Context, containerID, srcPath st | |||||
| 1505 | func extractFileFromTar(reader io.ReadCloser, targetPath string) (io.ReadCloser, error) { | 1520 | func extractFileFromTar(reader io.ReadCloser, targetPath string) (io.ReadCloser, error) { | ||
| 1506 | tr := tar.NewReader(reader) | 1521 | tr := tar.NewReader(reader) | ||
| 1507 | targetName := normalizedTarPath(targetPath) | 1522 | targetName := normalizedTarPath(targetPath) | ||
| 1523 | + | targetBase := filepath.Base(targetName) | |||
| 1524 | + | var fallback *removeOnCloseFile | |||
| 1525 | + | fallbackMatches := 0 | |||
| 1508 | 1526 | ||||
| 1509 | for { | 1527 | for { | ||
| 1510 | header, err := tr.Next() | 1528 | header, err := tr.Next() | ||
| @@ -1512,29 +1530,79 @@ func extractFileFromTar(reader io.ReadCloser, targetPath string) (io.ReadCloser, | |||||
| 1512 | break | 1530 | break | ||
| 1513 | } | 1531 | } | ||
| 1514 | if err != nil { | 1532 | if err != nil { | ||
| 1533 | + | cleanupFallback(fallback) | |||
| 1534 | + | _ = reader.Close() | |||
| 1515 | return nil, err | 1535 | return nil, err | ||
| 1516 | } | 1536 | } | ||
| 1537 | + | if header.Typeflag != tar.TypeReg { | |||
| 1538 | + | continue | |||
| 1539 | + | } | |||
| 1517 | 1540 | ||||
| 1518 | - | if header.Typeflag == tar.TypeReg && normalizedTarPath(header.Name) == targetName { | 1541 | + | headerName := normalizedTarPath(header.Name) |
| 1519 | - | size := header.Size | 1542 | + | if headerName == targetName { |
| 1520 | - | pr, pw := io.Pipe() | 1543 | + | cleanupFallback(fallback) |
| 1521 | - | go func() { | 1544 | + | return streamCurrentTarFile(reader, tr, header.Size), nil |
| 1522 | - | defer reader.Close() | 1545 | + | } |
| 1523 | - | _, copyErr := io.CopyN(pw, tr, size) | 1546 | + | if headerName == targetBase { |
| 1524 | - | if copyErr != nil { | 1547 | + | fallbackMatches++ |
| 1525 | - | _ = pw.CloseWithError(copyErr) | 1548 | + | if fallbackMatches == 1 { |
| 1526 | - | return | 1549 | + | fallback, err = bufferCurrentTarFile(tr, header.Size) |
| 1550 | + | if err != nil { | |||
| 1551 | + | _ = reader.Close() | |||
| 1552 | + | return nil, err | |||
| 1527 | } | 1553 | } | ||
| 1528 | - | _ = pw.Close() | 1554 | + | } |
| 1529 | - | }() | |||
| 1530 | - | return &pipeReadCloser{pr: pr, pw: pw, original: reader}, nil | |||
| 1531 | } | 1555 | } | ||
| 1532 | } | 1556 | } | ||
| 1533 | 1557 | ||||
| 1534 | _ = reader.Close() | 1558 | _ = reader.Close() | ||
| 1559 | + | if fallbackMatches == 1 { | |||
| 1560 | + | if _, err := fallback.Seek(0, io.SeekStart); err != nil { | |||
| 1561 | + | cleanupFallback(fallback) | |||
| 1562 | + | return nil, err | |||
| 1563 | + | } | |||
| 1564 | + | return fallback, nil | |||
| 1565 | + | } | |||
| 1566 | + | cleanupFallback(fallback) | |||
| 1567 | + | if fallbackMatches > 1 { | |||
| 1568 | + | return nil, fmt.Errorf("ambiguous basename matches in container archive for: %s", targetPath) | |||
| 1569 | + | } | |||
| 1535 | return nil, fmt.Errorf("file not found in container: %s", targetPath) | 1570 | return nil, fmt.Errorf("file not found in container: %s", targetPath) | ||
| 1536 | } | 1571 | } | ||
| 1537 | 1572 | ||||
| 1573 | + | func streamCurrentTarFile(reader io.ReadCloser, tr *tar.Reader, size int64) io.ReadCloser { | |||
| 1574 | + | pr, pw := io.Pipe() | |||
| 1575 | + | go func() { | |||
| 1576 | + | defer reader.Close() | |||
| 1577 | + | _, copyErr := io.CopyN(pw, tr, size) | |||
| 1578 | + | if copyErr != nil { | |||
| 1579 | + | _ = pw.CloseWithError(copyErr) | |||
| 1580 | + | return | |||
| 1581 | + | } | |||
| 1582 | + | _ = pw.Close() | |||
| 1583 | + | }() | |||
| 1584 | + | return &pipeReadCloser{pr: pr, pw: pw, original: reader} | |||
| 1585 | + | } | |||
| 1586 | + | ||||
| 1587 | + | func bufferCurrentTarFile(tr *tar.Reader, size int64) (*removeOnCloseFile, error) { | |||
| 1588 | + | file, err := os.CreateTemp("", "gordon-copy-from-container-*") | |||
| 1589 | + | if err != nil { | |||
| 1590 | + | return nil, err | |||
| 1591 | + | } | |||
| 1592 | + | fallback := &removeOnCloseFile{File: file, path: file.Name()} | |||
| 1593 | + | if _, err := io.CopyN(fallback, tr, size); err != nil { | |||
| 1594 | + | cleanupFallback(fallback) | |||
| 1595 | + | return nil, err | |||
| 1596 | + | } | |||
| 1597 | + | return fallback, nil | |||
| 1598 | + | } | |||
| 1599 | + | ||||
| 1600 | + | func cleanupFallback(fallback *removeOnCloseFile) { | |||
| 1601 | + | if fallback != nil { | |||
| 1602 | + | _ = fallback.Close() | |||
| 1603 | + | } | |||
| 1604 | + | } | |||
| 1605 | + | ||||
| 1538 | func normalizedTarPath(path string) string { | 1606 | func normalizedTarPath(path string) string { | ||
| 1539 | path = filepath.ToSlash(filepath.Clean(path)) | 1607 | path = filepath.ToSlash(filepath.Clean(path)) | ||
| 1540 | return strings.TrimPrefix(path, "/") | 1608 | return strings.TrimPrefix(path, "/") | ||
M internal/adapters/out/docker/runtime_exec_test.go +65 -5
| @@ -54,15 +54,42 @@ func frameDockerStream(streamID byte, payload []byte) []byte { | |||
| 54 | 54 | func TestExtractFileFromTarMatchesFullCleanPath(t *testing.T) { | |
| 55 | 55 | var buf bytes.Buffer | |
| 56 | 56 | tw := tar.NewWriter(&buf) | |
| 57 | - | require.NoError(t, tw.WriteHeader(&tar.Header{Name: "other/.env", Typeflag: tar.TypeReg, Size: int64(len("wrong"))})) | |
| 58 | - | _, err := tw.Write([]byte("wrong")) | |
| 57 | + | require.NoError(t, writeTarFile(tw, "other/.env", "wrong")) | |
| 58 | + | require.NoError(t, writeTarFile(tw, "app/.env", "right")) | |
| 59 | + | require.NoError(t, tw.Close()) | |
| 60 | + | ||
| 61 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/app/.env") | |
| 59 | 62 | require.NoError(t, err) | |
| 60 | - | require.NoError(t, tw.WriteHeader(&tar.Header{Name: "app/.env", Typeflag: tar.TypeReg, Size: int64(len("right"))})) | |
| 61 | - | _, err = tw.Write([]byte("right")) | |
| 63 | + | defer rc.Close() | |
| 64 | + | ||
| 65 | + | data, err := io.ReadAll(rc) | |
| 62 | 66 | require.NoError(t, err) | |
| 67 | + | assert.Equal(t, "right", string(data)) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func TestExtractFileFromTarMatchesBasenameForCopiedFile(t *testing.T) { | |
| 71 | + | var buf bytes.Buffer | |
| 72 | + | tw := tar.NewWriter(&buf) | |
| 73 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "backup data")) | |
| 63 | 74 | require.NoError(t, tw.Close()) | |
| 64 | 75 | ||
| 65 | - | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/app/.env") | |
| 76 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") | |
| 77 | + | require.NoError(t, err) | |
| 78 | + | defer rc.Close() | |
| 79 | + | ||
| 80 | + | data, err := io.ReadAll(rc) | |
| 81 | + | require.NoError(t, err) | |
| 82 | + | assert.Equal(t, "backup data", string(data)) | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func TestExtractFileFromTarPrefersExactPathOverBasename(t *testing.T) { | |
| 86 | + | var buf bytes.Buffer | |
| 87 | + | tw := tar.NewWriter(&buf) | |
| 88 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "wrong")) | |
| 89 | + | require.NoError(t, writeTarFile(tw, "tmp/gordon-backup-test.bak", "right")) | |
| 90 | + | require.NoError(t, tw.Close()) | |
| 91 | + | ||
| 92 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") | |
| 66 | 93 | require.NoError(t, err) | |
| 67 | 94 | defer rc.Close() | |
| 68 | 95 | ||
| @@ -71,6 +98,39 @@ func TestExtractFileFromTarMatchesFullCleanPath(t *testing.T) { | |||
| 71 | 98 | assert.Equal(t, "right", string(data)) | |
| 72 | 99 | } | |
| 73 | 100 | ||
| 101 | + | func TestExtractFileFromTarRejectsAmbiguousBasenameFallback(t *testing.T) { | |
| 102 | + | var buf bytes.Buffer | |
| 103 | + | tw := tar.NewWriter(&buf) | |
| 104 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "first")) | |
| 105 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "second")) | |
| 106 | + | require.NoError(t, tw.Close()) | |
| 107 | + | ||
| 108 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") | |
| 109 | + | require.Error(t, err) | |
| 110 | + | assert.Contains(t, err.Error(), "ambiguous") | |
| 111 | + | assert.Nil(t, rc) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func TestExtractFileFromTarDoesNotMatchBasenameInNestedDirectory(t *testing.T) { | |
| 115 | + | var buf bytes.Buffer | |
| 116 | + | tw := tar.NewWriter(&buf) | |
| 117 | + | require.NoError(t, writeTarFile(tw, "other/gordon-backup-test.bak", "wrong")) | |
| 118 | + | require.NoError(t, tw.Close()) | |
| 119 | + | ||
| 120 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") | |
| 121 | + | require.Error(t, err) | |
| 122 | + | assert.Contains(t, err.Error(), "file not found") | |
| 123 | + | assert.Nil(t, rc) | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func writeTarFile(tw *tar.Writer, name, content string) error { | |
| 127 | + | if err := tw.WriteHeader(&tar.Header{Name: name, Typeflag: tar.TypeReg, Size: int64(len(content))}); err != nil { | |
| 128 | + | return err | |
| 129 | + | } | |
| 130 | + | _, err := tw.Write([]byte(content)) | |
| 131 | + | return err | |
| 132 | + | } | |
| 133 | + | ||
| 74 | 134 | func TestReadExtractedEnvFileRejectsOversizedContent(t *testing.T) { | |
| 75 | 135 | data, err := readExtractedEnvFile(io.NopCloser(strings.NewReader(strings.Repeat("a", maxExtractedEnvFileSize+1)))) | |
| 76 | 136 | require.Error(t, err) | |
| @@ -54,15 +54,42 @@ func frameDockerStream(streamID byte, payload []byte) []byte { | |||||
| 54 | func TestExtractFileFromTarMatchesFullCleanPath(t *testing.T) { | 54 | func TestExtractFileFromTarMatchesFullCleanPath(t *testing.T) { | ||
| 55 | var buf bytes.Buffer | 55 | var buf bytes.Buffer | ||
| 56 | tw := tar.NewWriter(&buf) | 56 | tw := tar.NewWriter(&buf) | ||
| 57 | - | require.NoError(t, tw.WriteHeader(&tar.Header{Name: "other/.env", Typeflag: tar.TypeReg, Size: int64(len("wrong"))})) | 57 | + | require.NoError(t, writeTarFile(tw, "other/.env", "wrong")) |
| 58 | - | _, err := tw.Write([]byte("wrong")) | 58 | + | require.NoError(t, writeTarFile(tw, "app/.env", "right")) |
| 59 | + | require.NoError(t, tw.Close()) | |||
| 60 | + | ||||
| 61 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/app/.env") | |||
| 59 | require.NoError(t, err) | 62 | require.NoError(t, err) | ||
| 60 | - | require.NoError(t, tw.WriteHeader(&tar.Header{Name: "app/.env", Typeflag: tar.TypeReg, Size: int64(len("right"))})) | 63 | + | defer rc.Close() |
| 61 | - | _, err = tw.Write([]byte("right")) | 64 | + | |
| 65 | + | data, err := io.ReadAll(rc) | |||
| 62 | require.NoError(t, err) | 66 | require.NoError(t, err) | ||
| 67 | + | assert.Equal(t, "right", string(data)) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func TestExtractFileFromTarMatchesBasenameForCopiedFile(t *testing.T) { | |||
| 71 | + | var buf bytes.Buffer | |||
| 72 | + | tw := tar.NewWriter(&buf) | |||
| 73 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "backup data")) | |||
| 63 | require.NoError(t, tw.Close()) | 74 | require.NoError(t, tw.Close()) | ||
| 64 | 75 | ||||
| 65 | - | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/app/.env") | 76 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") |
| 77 | + | require.NoError(t, err) | |||
| 78 | + | defer rc.Close() | |||
| 79 | + | ||||
| 80 | + | data, err := io.ReadAll(rc) | |||
| 81 | + | require.NoError(t, err) | |||
| 82 | + | assert.Equal(t, "backup data", string(data)) | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | func TestExtractFileFromTarPrefersExactPathOverBasename(t *testing.T) { | |||
| 86 | + | var buf bytes.Buffer | |||
| 87 | + | tw := tar.NewWriter(&buf) | |||
| 88 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "wrong")) | |||
| 89 | + | require.NoError(t, writeTarFile(tw, "tmp/gordon-backup-test.bak", "right")) | |||
| 90 | + | require.NoError(t, tw.Close()) | |||
| 91 | + | ||||
| 92 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") | |||
| 66 | require.NoError(t, err) | 93 | require.NoError(t, err) | ||
| 67 | defer rc.Close() | 94 | defer rc.Close() | ||
| 68 | 95 | ||||
| @@ -71,6 +98,39 @@ func TestExtractFileFromTarMatchesFullCleanPath(t *testing.T) { | |||||
| 71 | assert.Equal(t, "right", string(data)) | 98 | assert.Equal(t, "right", string(data)) | ||
| 72 | } | 99 | } | ||
| 73 | 100 | ||||
| 101 | + | func TestExtractFileFromTarRejectsAmbiguousBasenameFallback(t *testing.T) { | |||
| 102 | + | var buf bytes.Buffer | |||
| 103 | + | tw := tar.NewWriter(&buf) | |||
| 104 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "first")) | |||
| 105 | + | require.NoError(t, writeTarFile(tw, "gordon-backup-test.bak", "second")) | |||
| 106 | + | require.NoError(t, tw.Close()) | |||
| 107 | + | ||||
| 108 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") | |||
| 109 | + | require.Error(t, err) | |||
| 110 | + | assert.Contains(t, err.Error(), "ambiguous") | |||
| 111 | + | assert.Nil(t, rc) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func TestExtractFileFromTarDoesNotMatchBasenameInNestedDirectory(t *testing.T) { | |||
| 115 | + | var buf bytes.Buffer | |||
| 116 | + | tw := tar.NewWriter(&buf) | |||
| 117 | + | require.NoError(t, writeTarFile(tw, "other/gordon-backup-test.bak", "wrong")) | |||
| 118 | + | require.NoError(t, tw.Close()) | |||
| 119 | + | ||||
| 120 | + | rc, err := extractFileFromTar(io.NopCloser(bytes.NewReader(buf.Bytes())), "/tmp/gordon-backup-test.bak") | |||
| 121 | + | require.Error(t, err) | |||
| 122 | + | assert.Contains(t, err.Error(), "file not found") | |||
| 123 | + | assert.Nil(t, rc) | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func writeTarFile(tw *tar.Writer, name, content string) error { | |||
| 127 | + | if err := tw.WriteHeader(&tar.Header{Name: name, Typeflag: tar.TypeReg, Size: int64(len(content))}); err != nil { | |||
| 128 | + | return err | |||
| 129 | + | } | |||
| 130 | + | _, err := tw.Write([]byte(content)) | |||
| 131 | + | return err | |||
| 132 | + | } | |||
| 133 | + | ||||
| 74 | func TestReadExtractedEnvFileRejectsOversizedContent(t *testing.T) { | 134 | func TestReadExtractedEnvFileRejectsOversizedContent(t *testing.T) { | ||
| 75 | data, err := readExtractedEnvFile(io.NopCloser(strings.NewReader(strings.Repeat("a", maxExtractedEnvFileSize+1)))) | 135 | data, err := readExtractedEnvFile(io.NopCloser(strings.NewReader(strings.Repeat("a", maxExtractedEnvFileSize+1)))) | ||
| 76 | require.Error(t, err) | 136 | require.Error(t, err) | ||