master
go 260 lines 5.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package parity
4
5 import (
6 "bufio"
7 "fmt"
8 "io"
9 "os"
10 "regexp"
11 "sort"
12 "strconv"
13 "strings"
14 )
15
16 var snmpWalkLineRE = regexp.MustCompile(`^\s*\.?([0-9][0-9.]*)\s*=\s*([^:]+):\s*(.*)$`)
17
18 // WalkRecord is one normalized SNMP walk row.
19 type WalkRecord struct {
20 OID string `json:"oid" yaml:"oid"`
21 Type string `json:"type" yaml:"type"`
22 Value string `json:"value" yaml:"value"`
23 }
24
25 // WalkDataset is a parsed SNMP walk fixture with deterministic ordering.
26 type WalkDataset struct {
27 Path string `json:"path" yaml:"path"`
28 Records []WalkRecord `json:"records" yaml:"records"`
29 byOID map[string]WalkRecord
30 }
31
32 // LoadWalkFile parses a walk file in OpenNMS snmpwalk format.
33 func LoadWalkFile(path string) (WalkDataset, error) {
34 f, err := os.Open(path)
35 if err != nil {
36 return WalkDataset{}, fmt.Errorf("open walk file %q: %w", path, err)
37 }
38
39 records, err := ParseWalk(f)
40 if closeErr := f.Close(); err == nil && closeErr != nil {
41 return WalkDataset{}, fmt.Errorf("close walk file %q: %w", path, closeErr)
42 }
43 if err != nil {
44 return WalkDataset{}, fmt.Errorf("parse walk file %q: %w", path, err)
45 }
46
47 ds := WalkDataset{Path: path, Records: records, byOID: make(map[string]WalkRecord, len(records))}
48 for _, rec := range records {
49 ds.byOID[rec.OID] = rec
50 }
51 return ds, nil
52 }
53
54 // ParseWalk parses OpenNMS snmpwalk text records.
55 func ParseWalk(r io.Reader) ([]WalkRecord, error) {
56 scanner := bufio.NewScanner(r)
57 scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
58
59 type partial struct {
60 oid string
61 typ string
62 value strings.Builder
63 quoteOpen bool
64 escaped bool
65 }
66
67 var (
68 lineNo int
69 records []WalkRecord
70 cur *partial
71 )
72
73 flush := func() {
74 if cur == nil {
75 return
76 }
77 records = append(records, WalkRecord{
78 OID: cur.oid,
79 Type: cur.typ,
80 Value: normalizeWalkValue(cur.value.String()),
81 })
82 cur = nil
83 }
84
85 for scanner.Scan() {
86 lineNo++
87 line := strings.TrimRight(scanner.Text(), "\r")
88 if strings.TrimSpace(line) == "" {
89 if cur != nil {
90 cur.value.WriteByte('\n')
91 cur.quoteOpen, cur.escaped = updateQuoteState("\n", cur.quoteOpen, cur.escaped)
92 }
93 continue
94 }
95
96 if cur != nil {
97 if cur.value.Len() > 0 {
98 cur.value.WriteByte('\n')
99 cur.quoteOpen, cur.escaped = updateQuoteState("\n", cur.quoteOpen, cur.escaped)
100 }
101 cur.value.WriteString(line)
102 cur.quoteOpen, cur.escaped = updateQuoteState(line, cur.quoteOpen, cur.escaped)
103 if !cur.quoteOpen {
104 cur.quoteOpen = false
105 flush()
106 }
107 continue
108 }
109
110 match := snmpWalkLineRE.FindStringSubmatch(line)
111 if len(match) != 4 {
112 // Ignore non-record lines; these appear in a few vendor dumps.
113 continue
114 }
115
116 oid := normalizeOID(match[1])
117 typ := strings.TrimSpace(match[2])
118 value := match[3]
119 if oid == "" || typ == "" {
120 continue
121 }
122
123 quoteOpen, escaped := updateQuoteState(value, false, false)
124 if quoteOpen {
125 cur = &partial{oid: oid, typ: typ, quoteOpen: quoteOpen, escaped: escaped}
126 cur.value.WriteString(value)
127 continue
128 }
129
130 records = append(records, WalkRecord{
131 OID: oid,
132 Type: typ,
133 Value: normalizeWalkValue(value),
134 })
135 }
136
137 if err := scanner.Err(); err != nil {
138 return nil, fmt.Errorf("scan walk line %d: %w", lineNo, err)
139 }
140 if cur != nil && cur.quoteOpen {
141 return nil, fmt.Errorf("unterminated quoted value for OID %s", cur.oid)
142 }
143 return records, nil
144 }
145
146 // Lookup returns one record by OID. OID matching ignores leading dots.
147 func (d *WalkDataset) Lookup(oid string) (WalkRecord, bool) {
148 if d == nil {
149 return WalkRecord{}, false
150 }
151 if len(d.byOID) == 0 {
152 index := make(map[string]WalkRecord, len(d.Records))
153 for _, rec := range d.Records {
154 index[rec.OID] = rec
155 }
156 d.byOID = index
157 }
158 rec, ok := d.byOID[normalizeOID(oid)]
159 return rec, ok
160 }
161
162 // Prefix returns all records with the given OID prefix, in file order.
163 func (d WalkDataset) Prefix(prefix string) []WalkRecord {
164 norm := normalizeOID(prefix)
165 if norm == "" {
166 out := make([]WalkRecord, len(d.Records))
167 copy(out, d.Records)
168 return out
169 }
170 out := make([]WalkRecord, 0)
171 for _, rec := range d.Records {
172 if strings.HasPrefix(rec.OID, norm) {
173 out = append(out, rec)
174 }
175 }
176 return out
177 }
178
179 // SortedOIDs returns deterministic OID ordering from the dataset.
180 func (d WalkDataset) SortedOIDs() []string {
181 oids := make([]string, 0, len(d.Records))
182 for _, rec := range d.Records {
183 oids = append(oids, rec.OID)
184 }
185 sort.Slice(oids, func(i, j int) bool {
186 return compareOID(oids[i], oids[j]) < 0
187 })
188 return oids
189 }
190
191 func compareOID(a, b string) int {
192 partsA := strings.Split(a, ".")
193 partsB := strings.Split(b, ".")
194 limit := min(len(partsB), len(partsA))
195
196 for i := range limit {
197 if partsA[i] == partsB[i] {
198 continue
199 }
200
201 numA, errA := strconv.Atoi(partsA[i])
202 numB, errB := strconv.Atoi(partsB[i])
203 if errA == nil && errB == nil {
204 switch {
205 case numA < numB:
206 return -1
207 case numA > numB:
208 return 1
209 default:
210 continue
211 }
212 }
213
214 if partsA[i] < partsB[i] {
215 return -1
216 }
217 return 1
218 }
219
220 switch {
221 case len(partsA) < len(partsB):
222 return -1
223 case len(partsA) > len(partsB):
224 return 1
225 default:
226 return 0
227 }
228 }
229
230 func normalizeOID(v string) string {
231 return strings.TrimLeft(strings.TrimSpace(v), ".")
232 }
233
234 func normalizeWalkValue(v string) string {
235 s := strings.TrimSpace(v)
236 if len(s) >= 2 && strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) {
237 s = strings.TrimPrefix(s, `"`)
238 s = strings.TrimSuffix(s, `"`)
239 }
240 s = strings.ReplaceAll(s, `\"`, `"`)
241 return s
242 }
243
244 func updateQuoteState(v string, quoteOpen, escaped bool) (bool, bool) {
245 for i := 0; i < len(v); i++ {
246 ch := v[i]
247 if escaped {
248 escaped = false
249 continue
250 }
251 if ch == '\\' {
252 escaped = true
253 continue
254 }
255 if ch == '"' {
256 quoteOpen = !quoteOpen
257 }
258 }
259 return quoteOpen, escaped
260 }