feat(filters): validate filter size (#1263)

* update comments

* refactor, fail on malformed size constraints

* refactor validation, add failing test

* unify in ReleaseSizeOkay, refactor test

* validate filter limit parseability

* logging improvement

* refactor. more clear, explicit parsing step

* inline, add log

* comment tweak

* pass error with more info

* tweak parsedSizeLimits interface
This commit is contained in:
Frederick Robinson 2023-11-20 09:41:53 -08:00 committed by GitHub
parent b7a8f6e6ed
commit 8d3921fd3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 149 additions and 131 deletions

View file

@ -5,6 +5,8 @@ package domain
import (
"context"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
@ -243,6 +245,18 @@ type FilterUpdate struct {
Indexers []Indexer `json:"indexers,omitempty"`
}
func (f Filter) Validate() error {
if f.Name == "" {
return errors.New("validation: name can't be empty")
}
if _, _, err := f.parsedSizeLimits(); err != nil {
return fmt.Errorf("error validating filter size limits: %w", err)
}
return nil
}
func (f Filter) CheckFilter(r *Release) ([]string, bool) {
// reset rejections first to clean previous checks
r.resetRejections()
@ -557,50 +571,24 @@ func (f Filter) isPerfectFLAC(r *Release) bool {
return true
}
// checkSizeFilter additional size check
// for indexers that doesn't announce size, like some gazelle based
// set flag r.AdditionalSizeCheckRequired if there's a size in the filter, otherwise go a head
// implement API for ptp,btn,ggn to check for size if needed
// for others pull down torrent and do check
// checkSizeFilter compares the filter size limits to a release's size if it is
// known from the announce line.
func (f Filter) checkSizeFilter(r *Release, minSize string, maxSize string) bool {
if r.Size == 0 {
r.AdditionalSizeCheckRequired = true
return true
} else {
r.AdditionalSizeCheckRequired = false
}
// if r.Size parse filter to bytes and compare
// handle both min and max
if minSize != "" {
// string to bytes
minSizeBytes, err := humanize.ParseBytes(minSize)
if err != nil {
r.addRejectionF("size: invalid minSize set: %s err: %q", minSize, err)
return false
}
if r.Size <= minSizeBytes {
r.addRejection("size: smaller than min size")
return false
}
sizeErr, err := f.CheckReleaseSize(r.Size)
if err != nil {
r.addRejectionF("size: error checking release size against filter: %+v", err)
return false
}
if maxSize != "" {
// string to bytes
maxSizeBytes, err := humanize.ParseBytes(maxSize)
if err != nil {
r.addRejectionF("size: invalid maxSize set: %s err: %q", maxSize, err)
return false
}
if r.Size >= maxSizeBytes {
r.addRejection("size: larger than max size")
return false
}
if sizeErr != nil {
r.addRejectionF("%+v", sizeErr)
return false
}
return true
@ -934,3 +922,50 @@ func matchHDR(releaseValues []string, filterValues []string) bool {
return false
}
func (f Filter) CheckReleaseSize(releaseSize uint64) (sizeErr, err error) {
min, max, err := f.parsedSizeLimits()
if err != nil {
return err, err
}
if min != nil && releaseSize <= *min {
return fmt.Errorf("release size %d bytes <= min size %d bytes", releaseSize, *min), nil
}
if max != nil && releaseSize >= *max {
return fmt.Errorf("release size %d bytes <= max size %d bytes", releaseSize, *max), nil
}
return nil, nil
}
// parsedSizeLimits parses filter bytes limits (expressed as a string) into a
// uint64 number of bytes. The bounds are returned as *uint64 number of bytes,
// with "nil" representing "no limit". We break out filter size limit parsing
// into a discrete step so that we can more easily check parsability at filter
// creation time.
func (f Filter) parsedSizeLimits() (*uint64, *uint64, error) {
min, err := parseBytes(f.MinSize)
if err != nil {
return nil, nil, fmt.Errorf("trouble parsing min size: %w", err)
}
max, err := parseBytes(f.MaxSize)
if err != nil {
return nil, nil, fmt.Errorf("trouble parsing max size: %w", err)
}
return min, max, nil
}
// parseBytes parses a string representation of a file size into a number of
// bytes. It returns a *uint64 where "nil" represents "none" (corresponding to
// the empty string)
func parseBytes(s string) (*uint64, error) {
if s == "" {
return nil, nil
}
b, err := humanize.ParseBytes(s)
return &b, err
}

View file

@ -2114,3 +2114,60 @@ func Test_matchRegex(t *testing.T) {
})
}
}
func Test_validation(t *testing.T) {
tests := []struct {
name string
filter Filter
valid bool
}{
{name: "empty name", filter: Filter{}, valid: false},
{name: "empty filter, with name", filter: Filter{Name: "test"}, valid: true},
{name: "valid size limit", filter: Filter{Name: "test", MaxSize: "12MB"}, valid: true},
{name: "gibberish max size limit", filter: Filter{Name: "test", MaxSize: "asdf"}, valid: false},
{name: "gibberish min size limit", filter: Filter{Name: "test", MinSize: "qwerty"}, valid: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.valid, tt.filter.Validate() == nil, "validation error \"%+v\" in test case %s", tt.filter.Validate(), tt.filter.Name)
})
}
}
func Test_checkSizeFilter(t *testing.T) {
type args struct {
minSize string
maxSize string
releaseSize uint64
}
tests := []struct {
name string
filter Filter
releaseSize uint64
want bool
wantErr bool
}{
{name: "test_1", filter: Filter{MinSize: "1GB", MaxSize: ""}, releaseSize: 100, want: false, wantErr: false},
{name: "test_2", filter: Filter{MinSize: "1GB", MaxSize: ""}, releaseSize: 2000000000, want: true, wantErr: false},
{name: "test_3", filter: Filter{MinSize: "1GB", MaxSize: "2.2GB"}, releaseSize: 2000000000, want: true, wantErr: false},
{name: "test_4", filter: Filter{MinSize: "1GB", MaxSize: "2GIB"}, releaseSize: 2000000000, want: true, wantErr: false},
{name: "test_5", filter: Filter{MinSize: "1GB", MaxSize: "2GB"}, releaseSize: 2000000010, want: false, wantErr: false},
{name: "test_6", filter: Filter{MinSize: "1GB", MaxSize: "2GB"}, releaseSize: 2000000000, want: false, wantErr: false},
{name: "test_7", filter: Filter{MaxSize: "2GB"}, releaseSize: 2500000000, want: false, wantErr: false},
{name: "test_8", filter: Filter{MaxSize: "20GB"}, releaseSize: 2500000000, want: true, wantErr: false},
{name: "test_9", filter: Filter{MinSize: "unparseable", MaxSize: "20GB"}, releaseSize: 2500000000, want: false, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
checkErr, err := tt.filter.CheckReleaseSize(tt.releaseSize)
if err != nil != tt.wantErr {
t.Errorf("checkSizeFilter() error = %v, wantErr %v", err, tt.wantErr)
return
}
if checkErr == nil != tt.want {
t.Errorf("checkSizeFilter() got = %v, want %v", checkErr, tt.want)
}
})
}
}