diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..29ea93a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,22 @@ +version: "2" + +run: + timeout: 5m + modules-download-mode: readonly + +linters: + enable: + - errcheck + - govet + - staticcheck + - misspell + - gocritic + - revive + - ineffassign + - unused + - gocyclo + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + \ No newline at end of file diff --git a/Makefile b/Makefile index 0121bb9..a2a691e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ PROJECT := tar-diff -VERSION := $(shell awk -F'"' '/var VERSION =/ {print $$2}' pkg/common/version.go) + +VERSION := $(shell grep -oP 'VERSION\s*=\s*"\K[^"]+' pkg/protocol/version.go) + PROJ_TARBALL := $(PROJECT)_$(VERSION).tar.gz GOCOVERDIR := $(CURDIR)/test/coverage diff --git a/cmd/tar-diff/main.go b/cmd/tar-diff/main.go index aa87527..d998426 100644 --- a/cmd/tar-diff/main.go +++ b/cmd/tar-diff/main.go @@ -1,3 +1,4 @@ +// Package main implements the tar-diff command line tool for creating binary diffs between tar archives. package main import ( @@ -7,8 +8,8 @@ import ( "os" "path" - "github.com/containers/tar-diff/pkg/common" - tar_diff "github.com/containers/tar-diff/pkg/tar-diff" + "github.com/containers/tar-diff/pkg/protocol" + tardiff "github.com/containers/tar-diff/pkg/tar-diff" ) var version = flag.Bool("version", false, "Show version") @@ -26,7 +27,7 @@ func main() { flag.Parse() if *version { - fmt.Printf("%s %s\n", path.Base(os.Args[0]), common.VERSION) + fmt.Printf("%s %s\n", path.Base(os.Args[0]), protocol.VERSION) return } @@ -44,37 +45,21 @@ func main() { log.Fatalf("Error: %s", err) } - defer func() { - if err := oldFile.Close(); err != nil { - fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", oldFilename, err) - } - }() - newFile, err := os.Open(newFilename) if err != nil { log.Fatalf("Error: %s", err) } - defer func() { - if err := newFile.Close(); err != nil { - fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", newFilename, err) - } - }() deltaFile, err := os.Create(deltaFilename) if err != nil { log.Fatalf("Error: %s", err) } - options := tar_diff.NewOptions() + options := tardiff.NewOptions() options.SetCompressionLevel(*compressionLevel) options.SetMaxBsdiffFileSize(int64(*maxBsdiffSize) * 1024 * 1024) - err = tar_diff.Diff(oldFile, newFile, deltaFile, options) - if err != nil { - log.Fatalf("Error: %s", err) - } - - err = deltaFile.Close() + err = tardiff.Diff(oldFile, newFile, deltaFile, options) if err != nil { log.Fatalf("Error: %s", err) } diff --git a/cmd/tar-patch/main.go b/cmd/tar-patch/main.go index 4b49693..9cd0857 100644 --- a/cmd/tar-patch/main.go +++ b/cmd/tar-patch/main.go @@ -1,3 +1,4 @@ +// Package main implements the tar-patch command line tool for applying binary diffs to tar archives. package main import ( @@ -7,8 +8,8 @@ import ( "os" "path" - "github.com/containers/tar-diff/pkg/common" - tar_patch "github.com/containers/tar-diff/pkg/tar-patch" + "github.com/containers/tar-diff/pkg/protocol" + tarpatch "github.com/containers/tar-diff/pkg/tar-patch" ) var version = flag.Bool("version", false, "Show version") @@ -23,7 +24,7 @@ func main() { flag.Parse() if *version { - fmt.Printf("%s %s\n", path.Base(os.Args[0]), common.VERSION) + fmt.Printf("%s %s\n", path.Base(os.Args[0]), protocol.VERSION) return } @@ -36,22 +37,12 @@ func main() { extractedDir := flag.Arg(1) patchedFilename := flag.Arg(2) - dataSource := tar_patch.NewFilesystemDataSource(extractedDir) - defer func() { - if err := dataSource.Close(); err != nil { - fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", extractedDir, err) - } - }() + dataSource := tarpatch.NewFilesystemDataSource(extractedDir) deltaFile, err := os.Open(deltaFilename) if err != nil { log.Fatalf("Unable to open %s: %s", deltaFilename, err) } - defer func() { - if err := deltaFile.Close(); err != nil { - fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", deltaFilename, err) - } - }() var patchedFile *os.File @@ -63,14 +54,9 @@ func main() { if err != nil { log.Fatalf("Unable to create %s: %s", patchedFilename, err) } - defer func() { - if err := patchedFile.Close(); err != nil { - fmt.Fprintf(os.Stderr, "Error closing %s: %s\n", patchedFilename, err) - } - }() } - err = tar_patch.Apply(deltaFile, dataSource, patchedFile) + err = tarpatch.Apply(deltaFile, dataSource, patchedFile) if err != nil { log.Fatalf("Error applying diff: %s", err) } diff --git a/pkg/common/common.go b/pkg/common/common.go deleted file mode 100644 index c5b846b..0000000 --- a/pkg/common/common.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -const ( - DeltaOpData = iota - DeltaOpOpen = iota - DeltaOpCopy = iota - DeltaOpAddData = iota - DeltaOpSeek = iota -) - -var DeltaHeader = [...]byte{'t', 'a', 'r', 'd', 'f', '1', '\n', 0} diff --git a/pkg/common/version.go b/pkg/common/version.go deleted file mode 100644 index 2f68af1..0000000 --- a/pkg/common/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package common - -var VERSION = "v0.1.2" diff --git a/pkg/protocol/common.go b/pkg/protocol/common.go new file mode 100644 index 0000000..80af8d3 --- /dev/null +++ b/pkg/protocol/common.go @@ -0,0 +1,43 @@ +// Package protocol provides shared constants and data structures for tar-diff operations. +package protocol + +import "path/filepath" + +// Delta operation constants define the types of operations in a delta file. +const ( + DeltaOpData = iota // Raw data operation + DeltaOpOpen = iota // Open file operation + DeltaOpCopy = iota // Copy from source operation + DeltaOpAddData = iota // Add new data operation + DeltaOpSeek = iota // Seek operation +) + +// DeltaHeader is the magic header bytes for tar-diff files. +var DeltaHeader = [...]byte{'t', 'a', 'r', 'd', 'f', '1', '\n', 0} + +// CleanPath cleans up the path lexically and prevents path traversal attacks. +// Any ".." that extends outside the first elements (or the root itself) is invalid and returns "". +// Uses filepath.Clean for proper cross-platform path handling (Windows backslashes, drive letters). +// This is a security-critical function used by both tar-diff and tar-patch packages. +func CleanPath(pathName string) string { + // A path with a volume name is absolute and can lead to path traversal on Windows. + if filepath.VolumeName(pathName) != "" { + return "" + } + + // Convert to forward slashes for consistent processing + pathName = filepath.ToSlash(pathName) + + // We make the path always absolute, that way filepath.Clean() ensures it never goes outside the top ("root") dir + // even if its a relative path + clean := filepath.Clean(filepath.Join("/", pathName)) + + // Convert back to forward slashes to ensure consistent output + clean = filepath.ToSlash(clean) + + // We clean the initial slash, making all result relative (or "" which is error) + if len(clean) > 0 && clean[0] == '/' { + return clean[1:] + } + return "" +} diff --git a/pkg/protocol/version.go b/pkg/protocol/version.go new file mode 100644 index 0000000..6b21cc9 --- /dev/null +++ b/pkg/protocol/version.go @@ -0,0 +1,4 @@ +package protocol + +// VERSION contains the current version string for the tar-diff tool. +var VERSION = "v0.2.0" diff --git a/pkg/tar-diff/analysis.go b/pkg/tar-diff/analysis.go index 934bba0..2c6067a 100644 --- a/pkg/tar-diff/analysis.go +++ b/pkg/tar-diff/analysis.go @@ -1,4 +1,5 @@ -package tar_diff +// Package tardiff provides functionality for analyzing and creating binary differences between tar archives. +package tardiff import ( "archive/tar" @@ -11,6 +12,7 @@ import ( "strings" "github.com/containers/image/v5/pkg/compression" + "github.com/containers/tar-diff/pkg/protocol" ) type tarFileInfo struct { @@ -80,14 +82,6 @@ func isSparseFile(hdr *tar.Header) bool { // Cleans up the path lexically // Any ".." that extends outside the first elements (or the root itself) is invalid and returns "" -func cleanPath(pathName string) string { - // We make the path always absolute, that way path.Clean() ensure it never goes outside the top ("root") dir - // even if its a relative path - clean := path.Clean("/" + pathName) - - // We clean the initial slash, making all result relative (or "" which is error) - return clean[1:] -} // Ignore all the files that make no sense to either delta or re-use as is func useTarFile(hdr *tar.Header, cleanPath string) bool { @@ -143,16 +137,15 @@ func analyzeTar(tarMaybeCompressed io.Reader) (*tarInfo, error) { if err != nil { if err == io.EOF { break // Expected error - } else { - return nil, err } + return nil, err } // Normalize name, for safety - pathname := cleanPath(hdr.Name) + pathname := protocol.CleanPath(hdr.Name) // Handle hardlinks if hdr.Typeflag == tar.TypeLink { - linkname := cleanPath(hdr.Linkname) + linkname := protocol.CleanPath(hdr.Linkname) if linkname != "" { // Store a copy of the header for later use hdrCopy := *hdr @@ -216,11 +209,10 @@ func isDeltaCandidate(file *tarFileInfo) bool { func nameIsSimilar(a *tarFileInfo, b *tarFileInfo, fuzzy int) bool { if fuzzy == 0 { return a.basename == b.basename - } else { - aa := strings.SplitAfterN(a.basename, ".", 2)[0] - bb := strings.SplitAfterN(b.basename, ".", 2)[0] - return aa == bb } + aa := strings.SplitAfterN(a.basename, ".", 2)[0] + bb := strings.SplitAfterN(b.basename, ".", 2)[0] + return aa == bb } // Check that two files are not wildly dissimilar in size. @@ -257,9 +249,8 @@ func extractDeltaData(tarMaybeCompressed io.Reader, sourceByIndex map[int]*sourc if err != nil { if err == io.EOF { break // Expected error - } else { - return err } + return err } info := sourceByIndex[index] if info != nil && info.usedForDelta { @@ -279,7 +270,7 @@ func abs(n int64) int64 { } return n } -func analyzeForDelta(old *tarInfo, new *tarInfo, oldFile io.Reader) (*deltaAnalysis, error) { +func analyzeForDelta(old *tarInfo, newTar *tarInfo, oldFile io.Reader) (*deltaAnalysis, error) { sourceInfos := make([]sourceInfo, 0, len(old.files)) for i := range old.files { sourceInfos = append(sourceInfos, sourceInfo{file: &old.files[i]}) @@ -297,10 +288,10 @@ func analyzeForDelta(old *tarInfo, new *tarInfo, oldFile io.Reader) (*deltaAnaly } } - targetInfos := make([]targetInfo, 0, len(new.files)+len(new.hardlinks)) + targetInfos := make([]targetInfo, 0, len(newTar.files)+len(newTar.hardlinks)) - for i := range new.files { - file := &new.files[i] + for i := range newTar.files { + file := &newTar.files[i] // First look for exact content match usedForDelta := false var source *sourceInfo @@ -359,14 +350,14 @@ func analyzeForDelta(old *tarInfo, new *tarInfo, oldFile io.Reader) (*deltaAnaly targetInfos = append(targetInfos, info) } - targetInfoByIndex := make(map[int]*targetInfo, len(new.files)+len(new.hardlinks)) + targetInfoByIndex := make(map[int]*targetInfo, len(newTar.files)+len(newTar.hardlinks)) for i := range targetInfos { t := &targetInfos[i] targetInfoByIndex[t.file.index] = t } // Add hardlinks to targetInfoByIndex - for i := range new.hardlinks { - hl := &new.hardlinks[i] + for i := range newTar.hardlinks { + hl := &newTar.hardlinks[i] info := targetInfo{hardlink: hl} targetInfos = append(targetInfos, info) targetInfoByIndex[hl.index] = &targetInfos[len(targetInfos)-1] diff --git a/pkg/tar-diff/analysis_test.go b/pkg/tar-diff/analysis_test.go index c79b419..f885244 100644 --- a/pkg/tar-diff/analysis_test.go +++ b/pkg/tar-diff/analysis_test.go @@ -1,4 +1,4 @@ -package tar_diff +package tardiff import ( "archive/tar" diff --git a/pkg/tar-diff/bsdiff.go b/pkg/tar-diff/bsdiff.go index ec5c41d..97d7aa9 100644 --- a/pkg/tar-diff/bsdiff.go +++ b/pkg/tar-diff/bsdiff.go @@ -1,4 +1,4 @@ -package tar_diff +package tardiff // This was originally taken from the bsdiff 4 code: @@ -36,11 +36,12 @@ import ( "bytes" ) +//nolint:gocyclo // bsdiff algorithm is inherently complex, refactoring would risk breaking the proven algorithm func bsdiff(oldbin, newbin []byte, deltaWriter *deltaWriter) error { iii := make([]int, len(oldbin)+1) qsufsort(iii, oldbin) - //var db + // var db var dblen, eblen, ebpos, slen int newsize := len(newbin) @@ -170,7 +171,7 @@ func bsdiff(oldbin, newbin []byte, deltaWriter *deltaWriter) error { return nil } -func min(a, b int) int { +func minInt(a, b int) int { if a < b { return a } @@ -194,7 +195,7 @@ func search(iii []int, oldbin []byte, newbin []byte, st, en int, pos *int) int { } x = st + (en-st)/2 - cmpln := min(oldsize-iii[x], newsize) + cmpln := minInt(oldsize-iii[x], newsize) if bytes.Compare(oldbin[iii[x]:iii[x]+cmpln], newbin[:cmpln]) < 0 { return search(iii, oldbin, newbin, x, en, pos) } @@ -322,12 +323,14 @@ func split(iii, vvv []int, start, ln, h int) { j = 0 k = 0 for i < jj { - if vvv[iii[i]+h] < x { + val := vvv[iii[i]+h] + switch { + case val < x: i++ - } else if vvv[iii[i]+h] == x { + case val == x: iii[i], iii[jj+j] = iii[jj+j], iii[i] j++ - } else { + default: iii[i], iii[kk+k] = iii[kk+k], iii[i] k++ } diff --git a/pkg/tar-diff/delta.go b/pkg/tar-diff/delta.go index 7728a6f..9776a4f 100644 --- a/pkg/tar-diff/delta.go +++ b/pkg/tar-diff/delta.go @@ -1,8 +1,8 @@ -package tar_diff +package tardiff import ( "encoding/binary" - "github.com/containers/tar-diff/pkg/common" + "github.com/containers/tar-diff/pkg/protocol" "github.com/klauspost/compress/zstd" "io" ) @@ -19,7 +19,7 @@ type deltaWriter struct { } func newDeltaWriter(writer io.Writer, compressionLevel int) (*deltaWriter, error) { - _, err := writer.Write(common.DeltaHeader[:]) + _, err := writer.Write(protocol.DeltaHeader[:]) if err != nil { return nil, err } @@ -55,7 +55,7 @@ func (d *deltaWriter) FlushBuffer() error { if len(d.buffer) == 0 { return nil } - err := d.writeOp(common.DeltaOpData, uint64(len(d.buffer)), d.buffer) + err := d.writeOp(protocol.DeltaOpData, uint64(len(d.buffer)), d.buffer) d.buffer = d.buffer[:0] return err } @@ -74,9 +74,8 @@ func (d *deltaWriter) WriteContent(data []byte) error { if len(d.buffer) >= deltaDataChunkSize { return d.FlushBuffer() - } else { - return nil } + return nil } // Switches to new file if needed and ensures we're at the start of it @@ -87,7 +86,7 @@ func (d *deltaWriter) SetCurrentFile(filename string) error { if err != nil { return err } - err = d.writeOp(common.DeltaOpOpen, uint64(len(nameBytes)), nameBytes) + err = d.writeOp(protocol.DeltaOpOpen, uint64(len(nameBytes)), nameBytes) if err != nil { return err } @@ -108,7 +107,7 @@ func (d *deltaWriter) Seek(pos uint64) error { return err } - err = d.writeOp(common.DeltaOpSeek, pos, nil) + err = d.writeOp(protocol.DeltaOpSeek, pos, nil) if err != nil { return err } @@ -124,7 +123,7 @@ func (d *deltaWriter) SeekForward(pos uint64) error { return err } - err = d.writeOp(common.DeltaOpSeek, d.currentPos, nil) + err = d.writeOp(protocol.DeltaOpSeek, d.currentPos, nil) if err != nil { return err } @@ -137,7 +136,7 @@ func (d *deltaWriter) CopyFile(size uint64) error { return err } - err = d.writeOp(common.DeltaOpCopy, size, nil) + err = d.writeOp(protocol.DeltaOpCopy, size, nil) if err != nil { return err } @@ -152,7 +151,7 @@ func (d *deltaWriter) WriteAddContent(data []byte) error { } size := uint64(len(data)) - err = d.writeOp(common.DeltaOpAddData, size, data) + err = d.writeOp(protocol.DeltaOpAddData, size, data) if err != nil { return err } diff --git a/pkg/tar-diff/diff.go b/pkg/tar-diff/diff.go index 23e3cbd..9ee2c1c 100644 --- a/pkg/tar-diff/diff.go +++ b/pkg/tar-diff/diff.go @@ -1,4 +1,4 @@ -package tar_diff +package tardiff import ( "archive/tar" @@ -157,7 +157,8 @@ func (g *deltaGenerator) generateForFile(info *targetInfo) error { maxBsdiffSize := g.options.maxBsdiffSize - if sourceFile.sha1 == file.sha1 && sourceFile.size == file.size { + switch { + case sourceFile.sha1 == file.sha1 && sourceFile.size == file.size: // Reuse exact file from old tar if err := g.deltaWriter.WriteOldFile(sourceFile.path, uint64(sourceFile.size)); err != nil { return err @@ -166,17 +167,17 @@ func (g *deltaGenerator) generateForFile(info *targetInfo) error { if err := g.skipRest(); err != nil { return err } - } else if maxBsdiffSize == 0 || (file.size < maxBsdiffSize && sourceFile.size < maxBsdiffSize) { + case maxBsdiffSize == 0 || (file.size < maxBsdiffSize && sourceFile.size < maxBsdiffSize): // Use bsdiff to generate delta if err := g.generateForFileWithBsdiff(info); err != nil { return err } - } else if info.rollsumMatches != nil && info.rollsumMatches.matchRatio > 20 { + case info.rollsumMatches != nil && info.rollsumMatches.matchRatio > 20: // Use rollsums to generate delta if err := g.generateForFileWithrollsums(info); err != nil { return err } - } else { + default: if err := g.copyRest(); err != nil { return err } @@ -223,9 +224,8 @@ func generateDelta(newFile io.ReadSeeker, deltaFile io.Writer, analysis *deltaAn if err == io.EOF { // Expected error break - } else { - return err } + return err } info := g.analysis.targetInfoByIndex[index] @@ -257,19 +257,23 @@ func generateDelta(newFile io.ReadSeeker, deltaFile io.Writer, analysis *deltaAn return nil } +// Options configures the behavior of the diff operation. type Options struct { compressionLevel int maxBsdiffSize int64 } +// SetCompressionLevel sets the compression level for the output diff file. func (o *Options) SetCompressionLevel(compressionLevel int) { o.compressionLevel = compressionLevel } +// SetMaxBsdiffFileSize sets the maximum file size for bsdiff operations. func (o *Options) SetMaxBsdiffFileSize(maxBsdiffSize int64) { o.maxBsdiffSize = maxBsdiffSize } +// NewOptions creates a new Options struct with default values. func NewOptions() *Options { return &Options{ compressionLevel: 3, @@ -277,6 +281,7 @@ func NewOptions() *Options { } } +// Diff creates a binary difference between two tar archives. func Diff(oldTarFile io.ReadSeeker, newTarFile io.ReadSeeker, diffFile io.Writer, options *Options) error { if options == nil { diff --git a/pkg/tar-diff/diff_test.go b/pkg/tar-diff/diff_test.go index c593c6a..e8fc674 100644 --- a/pkg/tar-diff/diff_test.go +++ b/pkg/tar-diff/diff_test.go @@ -1,4 +1,4 @@ -package tar_diff +package tardiff import ( "archive/tar" diff --git a/pkg/tar-diff/rollsum.go b/pkg/tar-diff/rollsum.go index 29405e7..77786b1 100644 --- a/pkg/tar-diff/rollsum.go +++ b/pkg/tar-diff/rollsum.go @@ -1,4 +1,4 @@ -package tar_diff +package tardiff import ( "hash" @@ -51,7 +51,7 @@ func (r *rollsum) add(drop byte, add byte) { } func (r *rollsum) roll(ch byte) { - r.blobSize += 1 + r.blobSize++ r.add(r.window[r.wofs], ch) r.window[r.wofs] = ch r.wofs = (r.wofs + 1) % bupWindowSize @@ -63,7 +63,7 @@ func (r *rollsum) shouldSplit() bool { } func (r *rollsum) init() { - r.blobStart = r.blobStart + r.blobSize + r.blobStart += r.blobSize r.blobSize = 0 r.blobCrc = crc32.NewIEEE() r.s1 = bupWindowSize * bupCharOffset diff --git a/pkg/tar-diff/stealerreader.go b/pkg/tar-diff/stealerreader.go index 818d5de..4a24757 100644 --- a/pkg/tar-diff/stealerreader.go +++ b/pkg/tar-diff/stealerreader.go @@ -1,4 +1,4 @@ -package tar_diff +package tardiff import ( "io" @@ -20,7 +20,7 @@ func newStealerReader(source io.Reader, stealer io.Writer) *stealerReader { func (s *stealerReader) Read(p []byte) (int, error) { n, err := s.source.Read(p) - var writeErr error = nil + var writeErr error if !s.ignore && n > 0 { _, writeErr = s.stealer.Write(p[0:n]) } diff --git a/pkg/tar-patch/apply.go b/pkg/tar-patch/apply.go index 032da7b..98c9afe 100644 --- a/pkg/tar-patch/apply.go +++ b/pkg/tar-patch/apply.go @@ -1,4 +1,5 @@ -package tar_patch +// Package tarpatch provides functionality for applying binary patches to tar archives. +package tarpatch import ( "bufio" @@ -7,35 +8,33 @@ import ( "fmt" "io" "os" - "path" "path/filepath" - "github.com/containers/tar-diff/pkg/common" + "github.com/containers/tar-diff/pkg/protocol" "github.com/klauspost/compress/zstd" ) +const ( + // maxFilenameSize prevents DoS attacks via excessively long filenames + maxFilenameSize = 4 * 1024 // 4KB limit for filenames + // maxAddDataSize prevents DoS attacks via excessive memory allocation for AddData operations + maxAddDataSize = 100 * 1024 * 1024 // 100MB limit for AddData operations +) + +// DataSource provides an interface for reading data during patch application. type DataSource interface { io.ReadSeeker io.Closer SetCurrentFile(file string) error } +// FilesystemDataSource implements DataSource by reading from filesystem files. type FilesystemDataSource struct { basePath string currentFile *os.File } -// Cleans up the path lexically -// Any ".." that extends outside the first elements (or the root itself) is invalid and returns "" -func cleanPath(pathName string) string { - // We make the path always absolute, that way path.Clean() ensure it never goes outside the top ("root") dir - // even if its a relative path - clean := path.Clean(path.Join("/", pathName)) - - // We clean the initial slash, making all result relative (or "" which is error) - return clean[1:] -} - +// NewFilesystemDataSource creates a new FilesystemDataSource with the specified base path. func NewFilesystemDataSource(basePath string) *FilesystemDataSource { return &FilesystemDataSource{ basePath: basePath, @@ -43,6 +42,7 @@ func NewFilesystemDataSource(basePath string) *FilesystemDataSource { } } +// Close closes the current file if one is open. func (f *FilesystemDataSource) Close() error { if f.currentFile != nil { err := f.currentFile.Close() @@ -62,6 +62,7 @@ func (f *FilesystemDataSource) Read(data []byte) (n int, err error) { return f.currentFile.Read(data) } +// SetCurrentFile opens the specified file for reading. func (f *FilesystemDataSource) SetCurrentFile(file string) error { if f.currentFile != nil { err := f.currentFile.Close() @@ -78,6 +79,7 @@ func (f *FilesystemDataSource) SetCurrentFile(file string) error { return nil } +// Seek changes the read position in the current file. func (f *FilesystemDataSource) Seek(offset int64, whence int) (int64, error) { if f.currentFile == nil { return 0, fmt.Errorf("no current file set") @@ -85,13 +87,14 @@ func (f *FilesystemDataSource) Seek(offset int64, whence int) (int64, error) { return f.currentFile.Seek(offset, whence) } +// Apply applies a binary patch from a delta reader to produce output using the data source. func Apply(delta io.Reader, dataSource DataSource, dst io.Writer) error { - buf := make([]byte, len(common.DeltaHeader)) + buf := make([]byte, len(protocol.DeltaHeader)) _, err := io.ReadFull(delta, buf) if err != nil { return err } - if !bytes.Equal(buf, common.DeltaHeader[:]) { + if !bytes.Equal(buf, protocol.DeltaHeader[:]) { return fmt.Errorf("invalid delta format") } @@ -118,19 +121,23 @@ func Apply(delta io.Reader, dataSource DataSource, dst io.Writer) error { } switch op { - case common.DeltaOpData: + case protocol.DeltaOpData: _, err = io.CopyN(dst, r, int64(size)) if err != nil { return err } - case common.DeltaOpOpen: + case protocol.DeltaOpOpen: + // Validate filename size to prevent DoS attacks + if size > maxFilenameSize { + return fmt.Errorf("filename size %d exceeds maximum allowed %d", size, maxFilenameSize) + } nameBytes := make([]byte, size) _, err = io.ReadFull(r, nameBytes) if err != nil { return err } name := string(nameBytes) - cleanName := cleanPath(name) + cleanName := protocol.CleanPath(name) if len(cleanName) == 0 { return fmt.Errorf("invalid source name '%v' in tar-diff", name) } @@ -138,12 +145,16 @@ func Apply(delta io.Reader, dataSource DataSource, dst io.Writer) error { if err != nil { return err } - case common.DeltaOpCopy: + case protocol.DeltaOpCopy: _, err = io.CopyN(dst, dataSource, int64(size)) if err != nil { return err } - case common.DeltaOpAddData: + case protocol.DeltaOpAddData: + // Validate AddData size to prevent DoS attacks via excessive memory allocation + if size > maxAddDataSize { + return fmt.Errorf("AddData operation size %d exceeds maximum allowed %d", size, maxAddDataSize) + } addBytes := make([]byte, size) _, err = io.ReadFull(r, addBytes) if err != nil { @@ -157,13 +168,13 @@ func Apply(delta io.Reader, dataSource DataSource, dst io.Writer) error { } for i := uint64(0); i < size; i++ { - addBytes[i] = addBytes[i] + addBytes2[i] + addBytes[i] += addBytes2[i] } if _, err := dst.Write(addBytes); err != nil { return err } - case common.DeltaOpSeek: + case protocol.DeltaOpSeek: _, err = dataSource.Seek(int64(size), io.SeekStart) if err != nil { return err