-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
In the case where we have a scenario that results in fewer than expected bytes being read from the tar compression or fewer than expected bytes written from the file writer consumer, propagate an error to the main program and exit with non-success status. Observed behavior shows that within the tar-extraction code path in certain circumstances a clean EOF is received by the tarReader and the archive is partially extracted. This generally presents as files missing in the destination directory. New code paths expect the correct number of bytes to be read or it will propagate a non-zero exit.
- Loading branch information
1 parent
7e7ec89
commit 0bc60c0
Showing
10 changed files
with
291 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
project_name: pget | ||
version: 2 | ||
before: | ||
hooks: | ||
- go mod tidy | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package consumer_test | ||
|
||
import ( | ||
"math/rand" | ||
) | ||
|
||
// generateTestContent generates a byte slice of a random size > 1KiB | ||
func generateTestContent(size int64) []byte { | ||
content := make([]byte, size) | ||
// Generate random bytes and write them to the content slice | ||
for i := range content { | ||
content[i] = byte(rand.Intn(256)) | ||
} | ||
return content | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,19 @@ | ||
package consumer | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
) | ||
|
||
type NullWriter struct{} | ||
|
||
var _ Consumer = &NullWriter{} | ||
|
||
func (NullWriter) Consume(reader io.Reader, destPath string) error { | ||
func (NullWriter) Consume(reader io.Reader, destPath string, expectedBytes int64) error { | ||
// io.Discard is explicitly designed to always succeed, ignore errors. | ||
_, _ = io.Copy(io.Discard, reader) | ||
bytesRead, _ := io.Copy(io.Discard, reader) | ||
if bytesRead != expectedBytes { | ||
return fmt.Errorf("expected %d bytes, read %d", expectedBytes, bytesRead) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package consumer_test | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/replicate/pget/pkg/consumer" | ||
) | ||
|
||
func TestNullWriter_Consume(t *testing.T) { | ||
buf := generateTestContent(1024) | ||
reader := bytes.NewReader(buf) | ||
|
||
nullConsumer := consumer.NullWriter{} | ||
err := nullConsumer.Consume(reader, "", 1024) | ||
assert.NoError(t, err) | ||
|
||
_, _ = reader.Seek(0, 0) | ||
err = nullConsumer.Consume(reader, "", 100) | ||
assert.Error(t, err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package consumer_test | ||
|
||
import ( | ||
"archive/tar" | ||
"bytes" | ||
"os" | ||
"path" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/replicate/pget/pkg/consumer" | ||
) | ||
|
||
const ( | ||
file1Content = "This is the content of file1." | ||
file2Content = "This is the content of file2." | ||
file1Path = "file1.txt" | ||
file2Path = "file2.txt" | ||
fileSymLinkPath = "link_to_file1.txt" | ||
fileHardLinkPath = "subdir/hard_link_to_file2.txt" | ||
) | ||
|
||
func createTarFileBytesBuffer() ([]byte, error) { | ||
// Create an in-memory representation of a tar file dynamically. This will be used to test the TarExtractor | ||
|
||
var buf bytes.Buffer | ||
tw := tar.NewWriter(&buf) | ||
|
||
// Create first file | ||
content1 := []byte(file1Content) | ||
hdr := &tar.Header{ | ||
Name: file1Path, | ||
Mode: 0600, | ||
Size: int64(len(content1)), | ||
ModTime: time.Now(), | ||
} | ||
if err := tw.WriteHeader(hdr); err != nil { | ||
return nil, err | ||
} | ||
if _, err := tw.Write(content1); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Create second file | ||
content2 := []byte(file2Content) | ||
hdr = &tar.Header{ | ||
Name: file2Path, | ||
Mode: 0600, | ||
Size: int64(len(content2)), | ||
ModTime: time.Now(), | ||
} | ||
if err := tw.WriteHeader(hdr); err != nil { | ||
return nil, err | ||
} | ||
if _, err := tw.Write(content2); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Create a symlink to file1 | ||
hdr = &tar.Header{ | ||
Name: fileSymLinkPath, | ||
Mode: 0777, | ||
Size: 0, | ||
Linkname: file1Path, | ||
Typeflag: tar.TypeSymlink, | ||
ModTime: time.Now(), | ||
} | ||
if err := tw.WriteHeader(hdr); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Create a subdirectory or path for the hardlink | ||
hdr = &tar.Header{ | ||
Name: "subdir/", | ||
Mode: 0755, | ||
Typeflag: tar.TypeDir, | ||
ModTime: time.Now(), | ||
} | ||
if err := tw.WriteHeader(hdr); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Create a hardlink to file2 in the subdirectory | ||
hdr = &tar.Header{ | ||
Name: fileHardLinkPath, | ||
Mode: 0600, | ||
Size: 0, | ||
Linkname: file2Path, | ||
Typeflag: tar.TypeLink, | ||
ModTime: time.Now(), | ||
} | ||
if err := tw.WriteHeader(hdr); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Close the tar writer to flush the data | ||
if err := tw.Close(); err != nil { | ||
return nil, err | ||
} | ||
|
||
return buf.Bytes(), nil | ||
} | ||
|
||
func TestTarExtractor_Consume(t *testing.T) { | ||
tarFileBytes, err := createTarFileBytesBuffer() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// Create a reader from the tar file bytes | ||
reader := bytes.NewReader(tarFileBytes) | ||
|
||
// Create a temporary directory to extract the tar file | ||
tmpDir, err := os.MkdirTemp("", "tarExtractorTest-") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.RemoveAll(tmpDir) | ||
|
||
tarConsumer := consumer.TarExtractor{} | ||
targetDir := path.Join(tmpDir, "extract") | ||
err = tarConsumer.Consume(reader, targetDir, int64(len(tarFileBytes))) | ||
assert.NoError(t, err) | ||
|
||
// Check if the extraction was successful | ||
checkTarExtraction(t, targetDir) | ||
|
||
// Test with incorrect expectedBytes | ||
_, _ = reader.Seek(0, 0) | ||
targetDir = path.Join(tmpDir, "extract-fail") | ||
err = tarConsumer.Consume(reader, targetDir, int64(len(tarFileBytes)-1)) | ||
assert.Error(t, err) | ||
} | ||
|
||
func checkTarExtraction(t *testing.T, targetDir string) { | ||
// Verify that file1.txt is correctly extracted | ||
fqFile1Path := path.Join(targetDir, file1Path) | ||
content, err := os.ReadFile(fqFile1Path) | ||
assert.NoError(t, err) | ||
assert.Equal(t, file1Content, string(content)) | ||
|
||
// Verify that file2.txt is correctly extracted | ||
fqFile2Path := path.Join(targetDir, file2Path) | ||
content, err = os.ReadFile(fqFile2Path) | ||
assert.NoError(t, err) | ||
assert.Equal(t, file2Content, string(content)) | ||
|
||
// Verify that link_to_file1.txt is a symlink pointing to file1.txt | ||
linkToFile1Path := path.Join(targetDir, fileSymLinkPath) | ||
linkTarget, err := os.Readlink(linkToFile1Path) | ||
assert.NoError(t, err) | ||
assert.Equal(t, file1Path, linkTarget) | ||
assert.Equal(t, os.ModeSymlink, os.ModeSymlink&os.ModeType) | ||
|
||
// Verify that subdir/hard_link_to_file2.txt is a hard link to file2.txt | ||
hardLinkToFile2Path := path.Join(targetDir, fileHardLinkPath) | ||
hardLinkStat, err := os.Stat(hardLinkToFile2Path) | ||
assert.NoError(t, err) | ||
file2Stat, err := os.Stat(fqFile2Path) | ||
assert.NoError(t, err) | ||
|
||
if !os.SameFile(hardLinkStat, file2Stat) { | ||
t.Errorf("hard link does not match file2.txt") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package consumer_test | ||
|
||
import ( | ||
"bytes" | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/replicate/pget/pkg/consumer" | ||
) | ||
|
||
func TestFileWriter_Consume(t *testing.T) { | ||
|
||
buf := generateTestContent(1024) | ||
reader := bytes.NewReader(buf) | ||
|
||
writeFileConsumer := consumer.FileWriter{} | ||
tmpFile, _ := os.CreateTemp("", "fileWriterTest-") | ||
|
||
defer tmpFile.Close() | ||
defer os.Remove(tmpFile.Name()) | ||
|
||
err := writeFileConsumer.Consume(reader, tmpFile.Name(), 1024) | ||
assert.NoError(t, err) | ||
|
||
// Check the file content is correct | ||
fileContent, _ := os.ReadFile(tmpFile.Name()) | ||
assert.Equal(t, buf, fileContent) | ||
|
||
_, _ = reader.Seek(0, 0) | ||
err = writeFileConsumer.Consume(reader, "", 100) | ||
assert.Error(t, err) | ||
|
||
// test overwrite | ||
// overwrite the file | ||
f, err := os.OpenFile(tmpFile.Name(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) | ||
assert.NoError(t, err) | ||
_, _ = f.Write([]byte("different content")) | ||
f.Close() | ||
|
||
// consume the reader | ||
_, _ = reader.Seek(0, 0) | ||
writeFileConsumer.Overwrite = true | ||
err = writeFileConsumer.Consume(reader, tmpFile.Name(), 1024) | ||
assert.NoError(t, err) | ||
|
||
// check the file content is correct | ||
fileContent, _ = os.ReadFile(tmpFile.Name()) | ||
assert.Equal(t, buf, fileContent) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters