• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokath / trice / 25233039650

01 May 2026 09:02PM UTC coverage: 90.93% (-0.09%) from 91.023%
25233039650

Pull #650

github

web-flow
Merge 924339d77 into ab3ce9d7b
Pull Request #650: Issue #649 fixed

194 of 231 new or added lines in 4 files covered. (83.98%)

1 existing line in 1 file now uncovered.

5504 of 6053 relevant lines covered (90.93%)

893.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.01
/internal/decoder/decoder.go
1
// SPDX-License-Identifier: MIT
2

3
// Package decoder provides several decoders for differently encoded trice streams.
4
package decoder
5

6
import (
7
        "encoding/binary"
8
        "fmt"
9
        "io"
10
        "path/filepath"
11
        "regexp"
12
        "strconv"
13
        "strings"
14
        "sync"
15
        "time"
16

17
        "github.com/rokath/trice/internal/emitter"
18
        "github.com/rokath/trice/internal/fmtspec"
19
        "github.com/rokath/trice/internal/id"
20
)
21

22
// TestTable is a struct slice generated by the trice tool -testTable option.
23
type TestTable []struct {
24
        In  []byte // byte buffer sequence
25
        Exp string // output
26
}
27

28
const (
29

30
        // LittleEndian is true for little endian trice data.
31
        LittleEndian = true
32

33
        // BigEndian is the flag value for target endianness.
34
        BigEndian = false
35

36
        // defaultSize is the beginning receive and sync buffer size.
37
        DefaultSize = 64 * 1024
38

39
        // hints is the help information in case of errors.
40
        Hints = "att:Hints:Baudrate? Encoding? Interrupt? Overflow? Parameter count? Format specifier? Password? til.json? Version?"
41

42
        UnsignedFormatSpecifier = 0 // %u -> %d
43
        SignedFormatSpecifier   = 1 // signed integer formatting
44
        FloatFormatSpecifier    = 2 // %f and relatives
45
        BooleanFormatSpecifier  = 3 // a %t (bool) found
46
        PointerFormatSpecifier  = 4 // a %p (pointer) found
47
        StringFormatSpecifier   = 5 // a %s found
48

49
        // LiFmtDefault is the default layout for location info (file and line).
50
        LiFmtDefault = "info:%21s%6d "
51
)
52

53
var (
54
        // Verbose gives more information on output if set. The value is injected from main packages.
55
        Verbose bool
56

57
        // ShowID is used as format string for displaying the first trice ID at the start of each line if not "".
58
        ShowID string
59

60
        // decoder.LastTriceID is last decoded ID. It is used for switch -showID.
61
        LastTriceID id.TriceID
62

63
        // TestTableMode is a special option for easy decoder test table generation.
64
        TestTableMode bool
65

66
        // Unsigned if true, forces hex and in values printed as unsigned values.
67
        Unsigned bool
68

69
        DebugOut                        = false        // DebugOut enables debug information.
70
        DumpLineByteCount               int            // DumpLineByteCount is the bytes per line for the dumpDec decoder.
71
        InitialCycle                    = true         // InitialCycle helps with automatic cycle counter checks.
72
        TargetTimestamp                 uint64         // targetTimestamp contains target specific timestamp value.
73
        TargetTimestampSize             int            // TargetTimestampSize is set in dependence of trice type.
74
        TargetLocation                  uint32         // targetLocation contains 16 bit file id in high and 16 bit line number in low part.
75
        TargetStamp                     string         // TargetTimeStampUnit is the target timestamps time base for default formatting.
76
        TargetStamp32                   string         // ShowTargetStamp32 is the format string for target timestamps.
77
        TargetStamp16                   string         // ShowTargetStamp16 is the format string for target timestamps.
78
        TargetStamp0                    string         // ShowTargetStamp0 is the format string for target timestamps.
79
        TargetStamp32Delta              string         // TargetStamp32Delta is the format string for 32-bit target timestamp deltas.
80
        TargetStamp16Delta              string         // TargetStamp16Delta is the format string for 16-bit target timestamp deltas.
81
        TargetStamp0Delta               string         // TargetStamp0Delta is the format string for delta column alignment without target timestamps.
82
        TargetTimeStampUnitPassed       bool           // TargetTimeStampUnitPassed is true when flag was TargetTimeStampUnit passed.
83
        ShowTargetStamp32Passed         bool           // ShowTargetStamp32Passed is true when flag was TargetTimeStamp32 passed.
84
        ShowTargetStamp16Passed         bool           // ShowTargetStamp16Passed is true when flag was TargetTimeStamp16 passed.
85
        ShowTargetStamp0Passed          bool           // ShowTargetStamp0Passed is true when flag was TargetTimeStamp0 passed.
86
        ShowTargetStamp32DeltaPassed    bool           // ShowTargetStamp32DeltaPassed is true when flag TargetStamp32Delta was passed.
87
        ShowTargetStamp16DeltaPassed    bool           // ShowTargetStamp16DeltaPassed is true when flag TargetStamp16Delta was passed.
88
        ShowTargetStamp0DeltaPassed     bool           // ShowTargetStamp0DeltaPassed is true when flag TargetStamp0Delta was passed.
89
        LocationInformationFormatString = LiFmtDefault // LocationInformationFormatString is the format string for target location: line number and file name.
90
        LiFmtDefaultLen                 int            // Compute as var from LocationInformationFormatString.
91
        TargetLocationExists            bool           // TargetLocationExists is set in dependence of p.COBSModeDescriptor. (obsolete)
92
        PackageFraming                  string         // Framing is used for packing. Valid values COBS, TCOBS, TCOBSv1 (same as TCOBS)
93
        IDBits                          = 14           // IDBits holds count of bits used for ID (used at least in trexDecoder)
94
        NewlineIndent                   = -1           // Used for trice messages containing several newlines in format string for formatting.
95
        TriceStatistics                 bool           // Keep the occurred count for each Trice log when Trice is closed.
96
        IDStat                          map[id.TriceID]int
97
)
98

99
func init() {
15✔
100
        IDStat = make(map[id.TriceID]int)
15✔
101
        LiFmtDefaultLen = locationInformationWidth()
15✔
102
}
15✔
103

104
// New abstracts the function type for a new decoder.
105
type New func(out io.Writer, lut id.TriceIDLookUp, m *sync.RWMutex, li id.TriceIDLookUpLI, in io.Reader, endian bool) Decoder
106

107
// Config contains common constructor parameters for all decoders.
108
type Config struct {
109
        Out         io.Writer
110
        LUT         id.TriceIDLookUp
111
        LUTMutex    *sync.RWMutex
112
        LI          id.TriceIDLookUpLI
113
        In          io.Reader
114
        Endian      bool
115
        NeedBuffers bool // NeedBuffers allocates B, B0 and InnerBuffer for framed decoders.
116
}
117

118
// Decoder is providing a byte reader returning decoded trice's.
119
// SetInput allows switching the input stream to a different source.
120
type Decoder interface {
121
        io.Reader
122
        SetInput(io.Reader)
123
}
124

125
// DecoderData is the common data struct for all decoders.
126
type DecoderData struct {
127
        W           io.Writer          // io.Stdout or the like
128
        In          io.Reader          // in is the inner reader, which is used to get raw bytes
129
        InnerBuffer []byte             // avoid repeated allocation (trex)
130
        IBuf        []byte             // iBuf holds unprocessed (raw) bytes for interpretation.
131
        B           []byte             // read buffer holds a single decoded TCOBS package, which can contain several trices.
132
        B0          []byte             // initial value for B
133
        Endian      bool               // endian is true for LittleEndian and false for BigEndian
134
        TriceSize   int                // trice head and payload size as number of bytes
135
        ParamSpace  int                // trice payload size after head
136
        SLen        int                // string length for TRICE_S
137
        Lut         id.TriceIDLookUp   // id look-up map for translation
138
        LutMutex    *sync.RWMutex      // to avoid concurrent map read and map write during map refresh triggered by filewatcher
139
        Li          id.TriceIDLookUpLI // location information map
140
        Trice       id.TriceFmt        // id.TriceFmt // received trice
141
}
142

143
// NewDecoderData initializes the common base fields for a decoder.
144
//
145
// Callers can request pre-allocated internal working buffers with NeedBuffers.
146
func NewDecoderData(c Config) DecoderData {
74✔
147
        if c.Out == nil {
113✔
148
                c.Out = io.Discard
39✔
149
        }
39✔
150
        if c.LUTMutex == nil {
131✔
151
                c.LUTMutex = new(sync.RWMutex)
57✔
152
        }
57✔
153
        d := DecoderData{
74✔
154
                W:        c.Out,
74✔
155
                In:       c.In,
74✔
156
                IBuf:     make([]byte, 0, DefaultSize),
74✔
157
                Endian:   c.Endian,
74✔
158
                Lut:      c.LUT,
74✔
159
                LutMutex: c.LUTMutex,
74✔
160
                Li:       c.LI,
74✔
161
        }
74✔
162
        if c.NeedBuffers {
106✔
163
                d.B = make([]byte, 0, DefaultSize)
32✔
164
                d.B0 = make([]byte, DefaultSize)
32✔
165
                d.InnerBuffer = make([]byte, DefaultSize)
32✔
166
        }
32✔
167
        return d
74✔
168
}
169

170
// SetInput allows switching the input stream to a different source.
171
//
172
// This function is for easier testing with cycle counters.
173
func (p *DecoderData) SetInput(r io.Reader) {
7✔
174
        p.In = r
7✔
175
}
7✔
176

177
// ReadU16 returns the 2 b bytes as uint16 according the specified endianness
178
func (p *DecoderData) ReadU16(b []byte) uint16 {
52✔
179
        if p.Endian {
103✔
180
                return binary.LittleEndian.Uint16(b)
51✔
181
        }
51✔
182
        return binary.BigEndian.Uint16(b)
1✔
183
}
184

185
// ReadU32 returns the 4 b bytes as uint32 according the specified endianness
186
func (p *DecoderData) ReadU32(b []byte) uint32 {
22✔
187
        if p.Endian {
43✔
188
                return binary.LittleEndian.Uint32(b)
21✔
189
        }
21✔
190
        return binary.BigEndian.Uint32(b)
1✔
191
}
192

193
// ReadU64 returns the 8 b bytes as uint64 according the specified endianness
194
func (p *DecoderData) ReadU64(b []byte) uint64 {
8✔
195
        if p.Endian {
15✔
196
                return binary.LittleEndian.Uint64(b)
7✔
197
        }
7✔
198
        return binary.BigEndian.Uint64(b)
1✔
199
}
200

201
// UReplaceN checks all format specifier in i and replaces %nu with %nd and returns that result as o.
202
//
203
// If a replacement took place on position k u[k] is 0. Afterwards len(u) is amount of found format specifiers.
204
// Additional, if UnsignedHex is true, for FormatX specifiers u[k] is also 1.
205
// If a float format specifier was found at position k, u[k] is 2,
206
// http://www.cplusplus.com/reference/cstdio/printf/
207
// https://www.codingunit.com/printf-format-specifiers-format-conversions-and-formatted-output
208
func UReplaceN(i string) (o string, u []int) {
39✔
209
        // Normalize valid C length modifiers first so that both the source scanner and
39✔
210
        // the runtime decoder share the same understanding. This fixes Issue #649 and
39✔
211
        // related %l... cases without changing the original format string stored in til.json.
39✔
212
        o, specs := fmtspec.Normalize(i)
39✔
213
        for _, spec := range specs {
79✔
214
                switch spec.Kind {
40✔
NEW
215
                case fmtspec.KindPointer:
×
216
                        // This would require `unsafe.Pointer(uintptr(n))` inside unSignedOrSignedOut.
×
217
                        // There are false positive windows vet warnings:
×
218
                        // https://stackoverflow.com/questions/43767898/casting-a-int-to-a-pointer
×
219
                        // https://github.com/golang/go/issues/41205
×
220
                        // As workaround replace %p with %x in the format strings.
×
NEW
221
                        o = replaceNextConversion(o, 'p', 'x')
×
NEW
222
                        u = append(u, PointerFormatSpecifier)
×
NEW
223
                case fmtspec.KindBool:
×
NEW
224
                        u = append(u, BooleanFormatSpecifier)
×
225
                case fmtspec.KindString:
3✔
226
                        u = append(u, StringFormatSpecifier)
3✔
227
                case fmtspec.KindFloat:
2✔
228
                        u = append(u, FloatFormatSpecifier)
2✔
229
                case fmtspec.KindUnsigned:
4✔
230
                        // Keep the longstanding Trice behavior: Go fmt prints unsigned payloads
4✔
231
                        // via %d while the decoder chooses an unsigned Go integer type below.
4✔
232
                        o = replaceNextConversion(o, 'u', 'd')
4✔
233
                        u = append(u, UnsignedFormatSpecifier)
4✔
234
                case fmtspec.KindBasedInteger:
7✔
235
                        // Keep `%d`/`%i` signed even when -unsigned is active. The historical
7✔
236
                        // `Unsigned` switch only affected the base-changing integer verbs handled
7✔
237
                        // by the old locX branch (%x/%X/%o/%O/%b/...). Issue #649 added the shared
7✔
238
                        // parser, but this behavior must stay intact to avoid changing `%d` into
7✔
239
                        // an unsigned decimal print, as seen in the intensive PC target tests.
7✔
240
                        if Unsigned {
10✔
241
                                u = append(u, UnsignedFormatSpecifier)
3✔
242
                        } else {
7✔
243
                                u = append(u, SignedFormatSpecifier)
4✔
244
                        }
4✔
245
                case fmtspec.KindSigned:
24✔
246
                        u = append(u, SignedFormatSpecifier)
24✔
247
                }
248
        }
249
        return
39✔
250
}
251

252
func replaceNextConversion(format string, from, to byte) string {
4✔
253
        for i := 0; i < len(format); i++ {
23✔
254
                if format[i] != '%' {
33✔
255
                        continue
14✔
256
                }
257
                if i+1 < len(format) && format[i+1] == '%' {
5✔
NEW
258
                        i++
×
UNCOV
259
                        continue
×
260
                }
261
                for j := i + 1; j < len(format); j++ {
11✔
262
                        if format[j] == from {
10✔
263
                                return format[:j] + string(to) + format[j+1:]
4✔
264
                        }
4✔
265
                        if isASCIIAlpha(format[j]) {
3✔
266
                                break
1✔
267
                        }
268
                }
269
        }
NEW
270
        return format
×
271
}
272

273
func isASCIIAlpha(b byte) bool {
2✔
274
        return ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z')
2✔
275
}
2✔
276

277
// Dump prints the byte slice as hex in one line
278
func Dump(w io.Writer, b []byte) {
6✔
279
        for _, x := range b {
13✔
280
                fmt.Fprintf(w, "%02x ", x)
7✔
281
        }
7✔
282
        fmt.Fprintln(w, "")
6✔
283
}
284

285
func RecordForStatistics(tid id.TriceID) {
26✔
286
        if !TriceStatistics && !emitter.AllStatistics {
50✔
287
                return
24✔
288
        }
24✔
289
        count := IDStat[tid]
2✔
290
        count++
2✔
291
        IDStat[tid] = count
2✔
292
}
293

294
var (
295
        IDLUT id.TriceIDLookUp
296
        LILUT id.TriceIDLookUpLI
297
)
298

299
func PrintTriceStatistics(w io.Writer) {
2✔
300
        if !TriceStatistics && !emitter.AllStatistics {
3✔
301
                return
1✔
302
        }
1✔
303
        var sum int
1✔
304
        fmt.Fprintf(w, "\nTrice Statistics:                  (n: Trice ends with no newline, if 0 ↴)\n\n")
1✔
305
        fmt.Fprintln(w, `   Count |       Location Information       | Line |  ID   |    Type    |n| Format String`)
1✔
306
        fmt.Fprintln(w, " ------- | -------------------------------- | ---- | ----- | ---------- |-|----------------------------------------")
1✔
307
        for tid, count := range IDStat {
2✔
308
                sum += count
1✔
309
                trice := IDLUT[tid]
1✔
310
                li := LILUT[tid]
1✔
311
                var found bool
1✔
312
                trice.Strg, found = strings.CutSuffix(trice.Strg, `\n`)
1✔
313
                newline := ` `
1✔
314
                if !found {
1✔
315
                        newline = `0`
×
316
                }
×
317
                file := id.ToLIPath(li.File)
1✔
318
                fmt.Fprintf(w, "%8d | %32s |%5d | %5d | %10s |%s| %s\n", count, file, li.Line, tid, trice.Type, newline, emitter.Colorize(trice.Strg))
1✔
319
        }
320
        fmt.Fprintln(w, " ------------------------------------------------------------------------------------------------------------------")
1✔
321
        fmt.Fprintf(w, "%8d Trice messsges\n", sum)
1✔
322
}
323

324
// LocationInformation returns optional location information for id.
325
func LocationInformation(tid id.TriceID, li id.TriceIDLookUpLI) string {
4✔
326
        if li != nil && LocationInformationFormatString != "off" && LocationInformationFormatString != "none" {
6✔
327
                if li, ok := li[tid]; ok {
3✔
328
                        return fmt.Sprintf(LocationInformationFormatString, filepath.Base(li.File), li.Line)
1✔
329
                } else {
2✔
330
                        return fmt.Sprintf(LocationInformationFormatString, "", 0)
1✔
331
                }
1✔
332
        } else {
2✔
333
                if Verbose {
3✔
334
                        return "no li"
1✔
335
                }
1✔
336
        }
337
        return ""
1✔
338
}
339

340
// locationInformationWidth computes byte count for location information.
341
func locationInformationWidth() (sum int) {
16✔
342
        re := regexp.MustCompile("[0-9]+")
16✔
343
        s := re.FindAllString(LocationInformationFormatString, -1)
16✔
344
        for _, n := range s {
48✔
345
                v, err := strconv.Atoi(n)
32✔
346
                if err != nil {
32✔
347
                        panic(err)
×
348
                }
349
                sum += v
32✔
350
        }
351
        return sum + 1 // plus space after line number
16✔
352
}
353

354
// CorrectWrappedTimestamp checks whether a 32-bit timestamp falls outside the valid range
355
// and virtually sets a 33rd bit by adding 2^32 seconds to it
356
func CorrectWrappedTimestamp(ts32 uint32) time.Time {
3✔
357

3✔
358
        const (
3✔
359
                minValidYear = 2000
3✔
360
                maxValidYear = 2038
3✔
361
                wrapOffset   = 1 << 32 // 2^32 seconds
3✔
362
        )
3✔
363

3✔
364
        // Interpret the timestamp as time.Time
3✔
365
        t := time.Unix(int64(ts32), 0).UTC()
3✔
366

3✔
367
        if t.Year() >= minValidYear && t.Year() <= maxValidYear {
4✔
368
                return t
1✔
369
        }
1✔
370

371
        // Apply wraparound correction by adding 2^32 seconds
372
        tWrapped := time.Unix(int64(ts32)+wrapOffset, 0).UTC()
2✔
373

2✔
374
        // If the corrected timestamp is plausible, return it
2✔
375
        if tWrapped.Year() > maxValidYear && tWrapped.Year() <= maxValidYear+100 {
3✔
376
                return tWrapped
1✔
377
        }
1✔
378

379
        // Fallback: return the original timestamp and print a warning
380
        fmt.Printf("WARNING: Timestamp %v (%d) is outside the expected year range\n", t, ts32)
1✔
381
        return t
1✔
382
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc