diff --git a/sentence.go b/sentence.go index 2619275..0bf38b2 100644 --- a/sentence.go +++ b/sentence.go @@ -110,32 +110,16 @@ func (p *SentenceParser) parseBaseSentence(raw string) (BaseSentence, error) { if raw == "" { return BaseSentence{}, errors.New("nmea: can not parse empty input") } - - var ( - tagBlock TagBlock - err error - ) - - if startOfTagBlock := strings.IndexByte(raw, TagBlockSep); startOfTagBlock != -1 { - // tag block is always at the start of line (unless IEC 61162-450). Starts with `\` and ends with `\` and has valid sentence - // following or - // - // Note: tag block group can span multiple lines but we only parse ones that have sentence - endOfTagBlock := strings.LastIndexByte(raw, TagBlockSep) - if endOfTagBlock <= startOfTagBlock { - return BaseSentence{}, fmt.Errorf("nmea: sentence tag block is missing '\\' at the end") - } - tagBlock, err = parseTagBlock(raw[startOfTagBlock+1 : endOfTagBlock]) - if err != nil { + tagBlock, tagBlockLen, err := ParseTagBlock(raw) + if err != nil { + return BaseSentence{}, err + } + if tagBlockLen > 0 && p.OnTagBlock != nil { + if err := p.OnTagBlock(tagBlock); err != nil { return BaseSentence{}, err } - if p.OnTagBlock != nil { - if err := p.OnTagBlock(tagBlock); err != nil { - return BaseSentence{}, err - } - } - raw = raw[endOfTagBlock+1:] } + raw = raw[tagBlockLen:] startIndex := strings.IndexAny(raw, SentenceStart+SentenceStartEncapsulated) if startIndex != 0 { diff --git a/tagblock.go b/tagblock.go index 0c06bac..43752ba 100644 --- a/tagblock.go +++ b/tagblock.go @@ -17,20 +17,26 @@ type TagBlock struct { Text string // TypeTextString valid character string, parameter -t } -func parseInt64(raw string) (int64, error) { - i, err := strconv.ParseInt(raw, 10, 64) - if err != nil { - return 0, fmt.Errorf("nmea: tagblock unable to parse uint64 [%s]", raw) +// ParseTagBlock parses tag blocks from a sentence string. +// The second return value is the length of the tag block prefix. +// See: https://gpsd.gitlab.io/gpsd/AIVDM.html#_nmea_tag_blocks +func ParseTagBlock(raw string) (TagBlock, int, error) { + startOfTagBlock := strings.IndexByte(raw, TagBlockSep) + if startOfTagBlock == -1 { + return TagBlock{}, 0, nil } - return i, nil -} - -// parseTagBlock adds support for tagblocks -// https://gpsd.gitlab.io/gpsd/AIVDM.html#_nmea_tag_blocks -func parseTagBlock(tags string) (TagBlock, error) { + // tag block is always at the start of line (unless IEC 61162-450). Starts with `\` and ends with `\` and has valid sentence + // following or + // + // Note: tag block group can span multiple lines but we only parse ones that have sentence + endOfTagBlock := strings.LastIndexByte(raw, TagBlockSep) + if endOfTagBlock <= startOfTagBlock { + return TagBlock{}, 0, fmt.Errorf("nmea: sentence tag block is missing '\\' at the end") + } + tags := raw[startOfTagBlock+1 : endOfTagBlock] sumSepIndex := strings.Index(tags, ChecksumSep) if sumSepIndex == -1 { - return TagBlock{}, fmt.Errorf("nmea: tagblock does not contain checksum separator") + return TagBlock{}, 0, fmt.Errorf("nmea: tagblock does not contain checksum separator") } var ( @@ -43,22 +49,21 @@ func parseTagBlock(tags string) (TagBlock, error) { // Validate the checksum if checksum != checksumRaw { - return TagBlock{}, fmt.Errorf("nmea: tagblock checksum mismatch [%s != %s]", checksum, checksumRaw) + return TagBlock{}, 0, fmt.Errorf("nmea: tagblock checksum mismatch [%s != %s]", checksum, checksumRaw) } items := strings.Split(tags[:sumSepIndex], ",") for _, item := range items { parts := strings.SplitN(item, ":", 2) if len(parts) != 2 { - return TagBlock{}, - fmt.Errorf("nmea: tagblock field is malformed (should be :) [%s]", item) + return TagBlock{}, 0, fmt.Errorf("nmea: tagblock field is malformed (should be :) [%s]", item) } key, value := parts[0], parts[1] switch key { case "c": // UNIX timestamp tagBlock.Time, err = parseInt64(value) if err != nil { - return TagBlock{}, err + return TagBlock{}, 0, err } case "d": // Destination ID tagBlock.Destination = value @@ -67,12 +72,12 @@ func parseTagBlock(tags string) (TagBlock, error) { case "n": // Line count tagBlock.LineCount, err = parseInt64(value) if err != nil { - return TagBlock{}, err + return TagBlock{}, 0, err } case "r": // Relative time tagBlock.RelativeTime, err = parseInt64(value) if err != nil { - return TagBlock{}, err + return TagBlock{}, 0, err } case "s": // Source ID tagBlock.Source = value @@ -80,5 +85,13 @@ func parseTagBlock(tags string) (TagBlock, error) { tagBlock.Text = value } } - return tagBlock, nil + return tagBlock, endOfTagBlock + 1, nil +} + +func parseInt64(raw string) (int64, error) { + i, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return 0, fmt.Errorf("nmea: tagblock unable to parse uint64 [%s]", raw) + } + return i, nil } diff --git a/tagblock_test.go b/tagblock_test.go index e171e2e..979863f 100644 --- a/tagblock_test.go +++ b/tagblock_test.go @@ -7,60 +7,66 @@ import ( ) var tagblocktests = []struct { - name string - raw string - err string - msg TagBlock + name string + raw string + err string + block TagBlock + len int }{ { name: "Test NMEA tag block", - raw: "s:Satelite_1,c:1553390539*62", - msg: TagBlock{ + raw: "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,2,3", + block: TagBlock{ Time: 1553390539, Source: "Satelite_1", }, + len: 30, }, { name: "Test NMEA tag block with head", - raw: "s:satelite,c:1564827317*25", - msg: TagBlock{ + raw: "\\s:satelite,c:1564827317*25\\!AIVDM,1,2,3", + block: TagBlock{ Time: 1564827317, Source: "satelite", }, + len: 28, }, { name: "Test unknown tag", - raw: "x:NorSat_1,c:1564827317*42", - msg: TagBlock{ + raw: "\\x:NorSat_1,c:1564827317*42\\!AIVDM,1,2,3", + block: TagBlock{ Time: 1564827317, Source: "", }, + len: 28, }, { name: "Test unix timestamp", - raw: "x:NorSat_1,c:1564827317*42", - msg: TagBlock{ + raw: "\\x:NorSat_1,c:1564827317*42\\!AIVDM,1,2,3", + block: TagBlock{ Time: 1564827317, Source: "", }, + len: 28, }, { name: "Test milliseconds timestamp", - raw: "x:NorSat_1,c:1564827317000*72", - msg: TagBlock{ + raw: "\\x:NorSat_1,c:1564827317000*72\\!AIVDM,1,2,3", + block: TagBlock{ Time: 1564827317000, Source: "", }, + len: 31, }, { name: "Test all input types", - raw: "s:satelite,c:1564827317,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*3F", - msg: TagBlock{ + raw: "\\s:satelite,c:1564827317,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*3F\\!AIVDM,1,2,3", + block: TagBlock{ Time: 1564827317, RelativeTime: 1553390539, Destination: "ara", @@ -69,56 +75,61 @@ var tagblocktests = []struct { Text: "helloworld", LineCount: 13, }, + len: 72, }, { name: "Test empty tag in tagblock", - raw: "s:satelite,,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*68", + raw: "\\s:satelite,,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*68\\!AIVDM,1,2,3", err: "nmea: tagblock field is malformed (should be :) []", }, { name: "Test Invalid checksum", - raw: "s:satelite,c:1564827317*49", + raw: "\\s:satelite,c:1564827317*49\\!AIVDM,1,2,3", err: "nmea: tagblock checksum mismatch [25 != 49]", }, { name: "Test no checksum", - raw: "s:satelite,c:156482731749", + raw: "\\s:satelite,c:156482731749\\!AIVDM,1,2,3", err: "nmea: tagblock does not contain checksum separator", }, { name: "Test invalid timestamp", - raw: "s:satelite,c:gjadslkg*30", + raw: "\\s:satelite,c:gjadslkg*30\\!AIVDM,1,2,3", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, { name: "Test invalid linecount", - raw: "s:satelite,n:gjadslkg*3D", + raw: "\\s:satelite,n:gjadslkg*3D\\!AIVDM,1,2,3", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, { name: "Test invalid relative time", - raw: "s:satelite,r:gjadslkg*21", + raw: "\\s:satelite,r:gjadslkg*21\\!AIVDM,1,2,3", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, + { + name: "Test no tagblock", + raw: "!AIVDM,1,2,3", + }, } -func TestTagBlock(t *testing.T) { +func TestParseTagBlock(t *testing.T) { for _, tt := range tagblocktests { t.Run(tt.name, func(t *testing.T) { - m, err := parseTagBlock(tt.raw) + b, n, err := ParseTagBlock(tt.raw) if tt.err != "" { assert.Error(t, err) - assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - assert.Equal(t, tt.msg, m) } + assert.Equal(t, tt.block, b) + assert.Equal(t, tt.len, n) }) } }