diff --git a/server/filesystem/compress.go b/server/filesystem/compress.go index 65c8b62..0c2cd8f 100644 --- a/server/filesystem/compress.go +++ b/server/filesystem/compress.go @@ -95,7 +95,7 @@ func (fs *Filesystem) SpaceAvailableForDecompression(ctx context.Context, dir st } defer f.Close() - format, _, err := archives.Identify(ctx, filepath.Base(file), f) + format, _, err := identifyArchive(ctx, file, f) if err != nil && !errors.Is(err, archives.NoMatch) { return err } @@ -172,7 +172,7 @@ func (fs *Filesystem) DecompressFile(ctx context.Context, dir string, file strin defer f.Close() // Identify the type of archive we are dealing with. - format, input, err := archives.Identify(ctx, filepath.Base(file), f) + format, input, err := identifyArchive(ctx, file, f) if err != nil { if errors.Is(err, archives.NoMatch) { return newFilesystemError(ErrCodeUnknownArchive, err) @@ -188,6 +188,18 @@ func (fs *Filesystem) DecompressFile(ctx context.Context, dir string, file strin }) } +// identifyArchive identifies the format of an archive by its contents, and only +// falls back to its file name when the contents are not recognized. Matching on +// the name first reads an archive with the wrong extension, such as a .tar.gz +// renamed to .zip, as a zip inside a gzip stream, which cannot be extracted. +func identifyArchive(ctx context.Context, file string, f ufs.File) (archives.Format, io.Reader, error) { + format, input, err := archives.Identify(ctx, "", f) + if errors.Is(err, archives.NoMatch) { + return archives.Identify(ctx, filepath.Base(file), f) + } + return format, input, err +} + // ExtractTransfer extracts the archive of a server that is being transferred to // this node into the server's directory. Every file is kept, including ones the // denylist would refuse, since they are the server's own files. diff --git a/server/filesystem/decompress_test.go b/server/filesystem/decompress_test.go index 885d4fe..55fd4cf 100644 --- a/server/filesystem/decompress_test.go +++ b/server/filesystem/decompress_test.go @@ -143,3 +143,30 @@ func TestDecompressZipSymlinkEntry(t *testing.T) { require.NoError(t, err) assert.Equal(t, "target.txt", target) } + +// An archive created by the compress endpoint and renamed to a different +// extension is still read as the format it actually is. +func TestDecompressRenamedArchive(t *testing.T) { + fs, rfs := NewFs() + t.Cleanup(func() { _ = os.RemoveAll(rfs.root) }) + + fs.SetDiskLimit(1 << 20) + require.NoError(t, fs.CreateDirectory("source", "/")) + require.NoError(t, rfs.CreateServerFileFromString("source/file.txt", "hello")) + + st, err := fs.CompressFiles("/", []string{"source"}) + require.NoError(t, err) + require.NoError(t, fs.Rename(st.Name(), "renamed.zip")) + require.NoError(t, os.RemoveAll(filepath.Join(rfs.root, "server", "source"))) + + // The formats matching an archive are tried in random order, so a single + // attempt can pass by chance. + for i := 0; i < 10; i++ { + require.NoError(t, fs.SpaceAvailableForDecompression(context.Background(), "/", "renamed.zip")) + require.NoError(t, fs.DecompressFile(context.Background(), "/", "renamed.zip")) + } + + got, err := os.ReadFile(filepath.Join(rfs.root, "server", "source", "file.txt")) + require.NoError(t, err) + assert.Equal(t, "hello", string(got)) +}