diff --git a/.golangci.yml b/.golangci.yml index 8ea38bd..7c2deec 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -48,7 +48,6 @@ linters: - makezero - misspell - nakedret - - nestif - nilerr - nilnil - noctx @@ -89,6 +88,7 @@ linters: - lll - maintidx - musttag + - nestif - nlreturn - paralleltest - tagliatelle diff --git a/cmd/parse-all/main.go b/cmd/parse-all/main.go index 1ea74db..5e633f2 100644 --- a/cmd/parse-all/main.go +++ b/cmd/parse-all/main.go @@ -15,7 +15,11 @@ import ( var igcExtensionRx = regexp.MustCompile(`(?i)\.igc\z`) func run() error { + allowInvalidChars := flag.Bool("allow-invalid-chars", true, "allow invalid characters") flag.Parse() + options := []igc.ParseOption{ + igc.WithAllowInvalidChars(*allowInvalidChars), + } for _, arg := range flag.Args() { if err := fs.WalkDir(os.DirFS(arg), ".", func(path string, dirEntry fs.DirEntry, err error) error { if err != nil { @@ -32,7 +36,7 @@ func run() error { return err } defer file.Close() - igcFile, err := igc.Parse(file) + igcFile, err := igc.Parse(file, options...) if err != nil { return err } diff --git a/igc.go b/igc.go index d53266c..bc9b499 100644 --- a/igc.go +++ b/igc.go @@ -245,11 +245,11 @@ type IGC struct { } // Parse parses an IGC from r. -func Parse(r io.Reader) (*IGC, error) { - return newParser().parse(r) +func Parse(r io.Reader, options ...ParseOption) (*IGC, error) { + return newParser(options...).parse(r) } // Parse parses an IGC from lines. -func ParseLines(lines []string) (*IGC, error) { - return newParser().parseLines(lines) +func ParseLines(lines []string, options ...ParseOption) (*IGC, error) { + return newParser(options...).parseLines(lines) } diff --git a/igc_test.go b/igc_test.go index 0623763..559b787 100644 --- a/igc_test.go +++ b/igc_test.go @@ -329,7 +329,7 @@ func TestParseLine(t *testing.T) { { name: "unknown_record_null", line: "\x00", - expectedErr: `1: "\x00": unknown record type`, + expectedErr: "1: \"\\x00\": unknown record type\n'\\x00': invalid character", }, { name: "empty line", @@ -953,7 +953,9 @@ func TestParseTestData(t *testing.T) { file, err := os.Open(filepath.Join("testdata", name)) assert.NoError(t, err) defer file.Close() - igc, err := igc.Parse(file) + igc, err := igc.Parse(file, + igc.WithAllowInvalidChars(true), + ) assert.NoError(t, err) assertEqualErrors(t, expectedErrorsByName[name], igc.Errs) }) diff --git a/parser.go b/parser.go index a57eca6..18651cf 100644 --- a/parser.go +++ b/parser.go @@ -55,6 +55,8 @@ func (e unknownRecordTypeError) Error() string { } var ( + invalidCharsRx = regexp.MustCompile(`([^\x20\x22-\x23\x25-\x29\x2b-\x5b\x5d\x5f-\x7d])`) + aRecordRx = regexp.MustCompile(`\AA([A-Z]{3})(.*)\z`) bRecordRx = regexp.MustCompile(`\AB(\d{2})(\d{2})(\d{2})(\d{2})(\d{5})([NS])(\d{3})(\d{5})([EW])([AV])([0-9\-]\d{4})([0-9\-]\d{4})(.*)\z`) cRecordRx = regexp.MustCompile(`\AC(\d{2})(\d{5})([NS])(\d{3})(\d{5})([EW])(.*)\z`) @@ -76,6 +78,7 @@ var ( ) type parser struct { + allowInvalidChars bool date time.Time prevTime time.Time cRecords []Record @@ -92,8 +95,16 @@ type parser struct { kRecordAdditions []BKRecordAddition } -func newParser() *parser { - return &parser{ +type ParseOption func(*parser) + +func WithAllowInvalidChars(allowInvalidChars bool) ParseOption { + return func(p *parser) { + p.allowInvalidChars = allowInvalidChars + } +} + +func newParser(options ...ParseOption) *parser { + p := &parser{ bRecordsAdditionsByTLC: make(map[string]*BKRecordAddition), latMinMul: 1, latMinDiv: 6e4, @@ -101,6 +112,10 @@ func newParser() *parser { lonMinDiv: 6e4, fracSecondMul: 1e9, } + for _, o := range options { + o(p) + } + return p } func (p *parser) parse(r io.Reader) (*IGC, error) { @@ -158,6 +173,22 @@ func (p *parser) parseLines(lines []string) (*IGC, error) { default: err = unknownRecordTypeError(line[0]) } + if !p.allowInvalidChars { + if match := invalidCharsRx.FindStringSubmatch(lineStr); match != nil { + invalidChar := match[1][0] + var invalidCharErr error + if '\x20' <= invalidChar && invalidChar <= '\x7f' { + invalidCharErr = fmt.Errorf("'%c': invalid character", invalidChar) + } else { + invalidCharErr = fmt.Errorf("'\\x%02x': invalid character", invalidChar) + } + if err == nil { + err = invalidCharErr + } else { + err = errors.Join(err, invalidCharErr) + } + } + } records = append(records, record) if err != nil { errs = append(errs, &Error{ diff --git a/parser_test.go b/parser_test.go index 6431ed4..eb40a2a 100644 --- a/parser_test.go +++ b/parser_test.go @@ -69,3 +69,50 @@ func TestIntPow(t *testing.T) { }) } } + +func TestReservedCharacters(t *testing.T) { + for c, expected := range map[byte]bool{ + '\r': true, + '\n': true, + ' ': false, + '!': true, + '"': false, + '#': false, + '$': true, + '%': false, + '&': false, + '\'': false, + '(': false, + ')': false, + '0': false, + '9': false, + ':': false, + ';': false, + '<': false, + '=': false, + '>': false, + '?': false, + '@': false, + 'A': false, + 'Z': false, + '[': false, + '\\': true, + ']': false, + '^': true, + '_': false, + '`': false, + 'a': false, + 'z': false, + '{': false, + '|': false, + '}': false, + '~': true, + '*': true, + '\x80': true, + '\xff': true, + } { + t.Run(string(c), func(t *testing.T) { + assert.Equal(t, expected, invalidCharsRx.MatchString(string(c))) + }) + } +}