Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions internal/par2/par2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
5 changes: 5 additions & 0 deletions internal/par2/par2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading