From 3059e61c2438c6f20dfe3491efb001b01b41757f Mon Sep 17 00:00:00 2001 From: javi11 Date: Sat, 11 Apr 2026 16:17:58 +0200 Subject: [PATCH] fix: align PAR2 slice size to multiple of 4 as required by spec The par2go library requires SliceSize to be a positive multiple of 4. When articleSize (from posting config) was not a multiple of 4, calculateParBlockSize returned it unaligned, causing par2 creation to fail with "SliceSize must be a positive multiple of 4". Extract maxPar2Blocks constant, add alignDown helper, and ensure all code paths produce a spec-compliant block size. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/par2/par2.go | 40 ++++++++++++++++++++++++++++---------- internal/par2/par2_test.go | 5 +++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/internal/par2/par2.go b/internal/par2/par2.go index 679e8b7..5e5787c 100644 --- a/internal/par2/par2.go +++ b/internal/par2/par2.go @@ -21,6 +21,11 @@ import ( var parregexp = regexp.MustCompile(`(?i)(\.vol\d+\+(\d+))?\.par2$`) +// maxPar2Blocks is the PAR2 specification limit for the maximum number of +// data + recovery blocks. The format uses 16-bit identifiers, so the +// theoretical maximum is 2^15 = 32768. +const maxPar2Blocks = 32768 + // Par2Executor defines the interface for executing par2 commands. type Par2Executor interface { Create(ctx context.Context, files []fileinfo.FileInfo) ([]string, error) @@ -226,6 +231,14 @@ func (p *NativeExecutor) createPar2ForFile(ctx context.Context, file fileinfo.Fi return nil, nil } } + // PAR2 spec requires slice size to be a multiple of 4 + parBlockSize = alignDown(parBlockSize, 4) + if parBlockSize < 4 { + slog.WarnContext(ctx, "Block size too small for PAR2 creation, skipping", + "path", file.Path, "size", file.Size) + return nil, nil + } + par2FileName := filepath.Base(file.Path) + ".par2" par2Path := filepath.Join(dirPath, par2FileName) @@ -371,17 +384,24 @@ func NewExecutor(articleSize uint64, cfg *config.Par2Config, jobProgress progres return New(articleSize, cfg, jobProgress) } +// alignDown rounds v down to the nearest multiple of alignment. +func alignDown(v, alignment uint64) uint64 { + return (v / alignment) * alignment +} + // calculateParBlockSize calculates the appropriate PAR2 block size for the given file. +// The returned value is always a multiple of 4 as required by the PAR2 specification. func calculateParBlockSize(fileSize uint64, articleSize uint64) uint64 { - maxParBlocks := uint64(32768) - - if fileSize/articleSize < maxParBlocks { - return articleSize - } - minParBlockSize := (fileSize / maxParBlocks) + 1 - multiplier := minParBlockSize / articleSize - if minParBlockSize%articleSize != 0 { - multiplier++ + var blockSize uint64 + if fileSize/articleSize < maxPar2Blocks { + blockSize = articleSize + } else { + minParBlockSize := (fileSize / maxPar2Blocks) + 1 + multiplier := minParBlockSize / articleSize + if minParBlockSize%articleSize != 0 { + multiplier++ + } + blockSize = multiplier * articleSize } - return multiplier * articleSize + return alignDown(blockSize, 4) } diff --git a/internal/par2/par2_test.go b/internal/par2/par2_test.go index bf07f0e..4ae9aea 100644 --- a/internal/par2/par2_test.go +++ b/internal/par2/par2_test.go @@ -218,6 +218,11 @@ func TestCalculateParBlockSize(t *testing.T) { {32768*512*1024 - 1, 512 * 1024, 512 * 1024}, {32768*512*1024*3 + 1, 512 * 1024, 512 * 1024 * 4}, {1024, 512 * 1024, 512 * 1024}, + // articleSize not a multiple of 4 — must be rounded down + {10 * 1024 * 1024, 361254, 361252}, + {10 * 1024 * 1024, 750001, 750000}, + // articleSize already a multiple of 4 + {10 * 1024 * 1024, 750000, 750000}, } for i, tc := range testCases {