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

essentialkaos / rep / 24768376680

22 Apr 2026 08:28AM UTC coverage: 96.38% (-0.2%) from 96.558%
24768376680

push

github

andyone
Fix tests

3115 of 3232 relevant lines covered (96.38%)

18.18 hits per line

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

97.18
/cli/query/query.go
1
package query
2

3
// ////////////////////////////////////////////////////////////////////////////////// //
4
//                                                                                    //
5
//                         Copyright (c) 2026 ESSENTIAL KAOS                          //
6
//      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
7
//                                                                                    //
8
// ////////////////////////////////////////////////////////////////////////////////// //
9

10
import (
11
        "fmt"
12
        "regexp"
13
        "strings"
14
        "time"
15

16
        "github.com/essentialkaos/ek/v14/fmtutil"
17
        "github.com/essentialkaos/ek/v14/mathutil"
18
        "github.com/essentialkaos/ek/v14/strutil"
19
        "github.com/essentialkaos/ek/v14/timeutil"
20

21
        "github.com/essentialkaos/rep/v3/repo/data"
22
        "github.com/essentialkaos/rep/v3/repo/search"
23
)
24

25
// ////////////////////////////////////////////////////////////////////////////////// //
26

27
const (
28
        TERM_SHORT_NAME        = "n"
29
        TERM_SHORT_VERSION     = "v"
30
        TERM_SHORT_RELEASE     = "r"
31
        TERM_SHORT_EPOCH       = "e"
32
        TERM_SHORT_ARCH        = "a"
33
        TERM_SHORT_SOURCE      = "s"
34
        TERM_SHORT_LICENSE     = "l"
35
        TERM_SHORT_GROUP       = "g"
36
        TERM_SHORT_VENDOR      = "V"
37
        TERM_SHORT_PROVIDES    = "P"
38
        TERM_SHORT_REQUIRES    = "R"
39
        TERM_SHORT_RECOMMENDS  = "RC"
40
        TERM_SHORT_CONFLICTS   = "C"
41
        TERM_SHORT_OBSOLETES   = "O"
42
        TERM_SHORT_ENHANCES    = "E"
43
        TERM_SHORT_SUGGESTS    = "SG"
44
        TERM_SHORT_SUPPLEMENTS = "SP"
45
        TERM_SHORT_FILE        = "f"
46
        TERM_SHORT_DATE_ADD    = "d"
47
        TERM_SHORT_DATE_BUILD  = "D"
48
        TERM_SHORT_BUILD_HOST  = "h"
49
        TERM_SHORT_SIZE        = "S"
50
        TERM_SHORT_PAYLOAD     = "@"
51

52
        TERM_NAME        = "name"
53
        TERM_VERSION     = "version"
54
        TERM_RELEASE     = "release"
55
        TERM_EPOCH       = "epoch"
56
        TERM_ARCH        = "arch"
57
        TERM_SOURCE      = "source"
58
        TERM_LICENSE     = "license"
59
        TERM_GROUP       = "group"
60
        TERM_VENDOR      = "vendor"
61
        TERM_PROVIDES    = "provides"
62
        TERM_REQUIRES    = "requires"
63
        TERM_RECOMMENDS  = "recommends"
64
        TERM_CONFLICTS   = "conflicts"
65
        TERM_OBSOLETES   = "obsoletes"
66
        TERM_ENHANCES    = "enhances"
67
        TERM_SUGGESTS    = "suggests"
68
        TERM_SUPPLEMENTS = "supplements"
69
        TERM_FILE        = "file"
70
        TERM_DATE_ADD    = "date-add"
71
        TERM_DATE_BUILD  = "date-build"
72
        TERM_BUILD_HOST  = "host"
73
        TERM_SIZE        = "size"
74
        TERM_PAYLOAD     = "payload"
75
)
76

77
const (
78
        TERM_SHORT_RELEASED = "^"
79

80
        TERM_RELEASED = "released"
81
)
82

83
const (
84
        FILTER_FLAG_NONE       uint8 = 0
85
        FILTER_FLAG_RELEASED   uint8 = 1
86
        FILTER_FLAG_UNRELEASED uint8 = 2
87
)
88

89
// ////////////////////////////////////////////////////////////////////////////////// //
90

91
// Request contains parsed query data
92
type Request struct {
93
        Query      search.Query
94
        FilterFlag uint8
95
}
96

97
// ////////////////////////////////////////////////////////////////////////////////// //
98

99
var terms = map[string]uint8{
100
        TERM_SHORT_NAME:        search.TERM_NAME,
101
        TERM_SHORT_VERSION:     search.TERM_VERSION,
102
        TERM_SHORT_RELEASE:     search.TERM_RELEASE,
103
        TERM_SHORT_EPOCH:       search.TERM_EPOCH,
104
        TERM_SHORT_PROVIDES:    search.TERM_PROVIDES,
105
        TERM_SHORT_REQUIRES:    search.TERM_REQUIRES,
106
        TERM_SHORT_RECOMMENDS:  search.TERM_RECOMMENDS,
107
        TERM_SHORT_CONFLICTS:   search.TERM_CONFLICTS,
108
        TERM_SHORT_OBSOLETES:   search.TERM_OBSOLETES,
109
        TERM_SHORT_ENHANCES:    search.TERM_ENHANCES,
110
        TERM_SHORT_SUGGESTS:    search.TERM_SUGGESTS,
111
        TERM_SHORT_SUPPLEMENTS: search.TERM_SUPPLEMENTS,
112
        TERM_SHORT_FILE:        search.TERM_FILE,
113
        TERM_SHORT_SOURCE:      search.TERM_SOURCE,
114
        TERM_SHORT_LICENSE:     search.TERM_LICENSE,
115
        TERM_SHORT_GROUP:       search.TERM_GROUP,
116
        TERM_SHORT_VENDOR:      search.TERM_VENDOR,
117
        TERM_SHORT_DATE_ADD:    search.TERM_DATE_ADD,
118
        TERM_SHORT_DATE_BUILD:  search.TERM_DATE_BUILD,
119
        TERM_SHORT_BUILD_HOST:  search.TERM_BUILD_HOST,
120
        TERM_SHORT_SIZE:        search.TERM_SIZE,
121
        TERM_SHORT_ARCH:        search.TERM_ARCH,
122
        TERM_SHORT_PAYLOAD:     search.TERM_PAYLOAD,
123

124
        TERM_NAME:        search.TERM_NAME,
125
        TERM_VERSION:     search.TERM_VERSION,
126
        TERM_RELEASE:     search.TERM_RELEASE,
127
        TERM_EPOCH:       search.TERM_EPOCH,
128
        TERM_PROVIDES:    search.TERM_PROVIDES,
129
        TERM_REQUIRES:    search.TERM_REQUIRES,
130
        TERM_RECOMMENDS:  search.TERM_RECOMMENDS,
131
        TERM_CONFLICTS:   search.TERM_CONFLICTS,
132
        TERM_OBSOLETES:   search.TERM_OBSOLETES,
133
        TERM_ENHANCES:    search.TERM_ENHANCES,
134
        TERM_SUGGESTS:    search.TERM_SUGGESTS,
135
        TERM_SUPPLEMENTS: search.TERM_SUPPLEMENTS,
136
        TERM_FILE:        search.TERM_FILE,
137
        TERM_SOURCE:      search.TERM_SOURCE,
138
        TERM_LICENSE:     search.TERM_LICENSE,
139
        TERM_GROUP:       search.TERM_GROUP,
140
        TERM_VENDOR:      search.TERM_VENDOR,
141
        TERM_DATE_ADD:    search.TERM_DATE_ADD,
142
        TERM_DATE_BUILD:  search.TERM_DATE_BUILD,
143
        TERM_BUILD_HOST:  search.TERM_BUILD_HOST,
144
        TERM_SIZE:        search.TERM_SIZE,
145
        TERM_ARCH:        search.TERM_ARCH,
146
        TERM_PAYLOAD:     search.TERM_PAYLOAD,
147
}
148

149
var extTerm = map[string]bool{
150
        TERM_SHORT_RELEASED: true,
151
        TERM_RELEASED:       true,
152
}
153

154
var depRegex = regexp.MustCompile(`([a-zA-Z0-9\._\-:\(\)\*]+)(>=|<=|>|<|=)?([0-9]:)?([0-9a-z\.\*]+)?-?(.*)?`)
155

156
// ////////////////////////////////////////////////////////////////////////////////// //
157

158
// Parse parses string with data and creates search query
159
func Parse(q []string) (*Request, error) {
14✔
160
        result := &Request{}
14✔
161

14✔
162
        for _, rawTerm := range q {
36✔
163
                if len(rawTerm) == 0 {
28✔
164
                        continue
6✔
165
                }
166

167
                termName, _, _ := extractTermInfo(rawTerm)
16✔
168

16✔
169
                if extTerm[termName] {
22✔
170
                        err := parseExtTerm(rawTerm, result)
6✔
171

6✔
172
                        if err != nil {
8✔
173
                                return nil, err
2✔
174
                        }
2✔
175
                } else {
10✔
176
                        term, err := parseTerm(rawTerm)
10✔
177

10✔
178
                        if err != nil {
12✔
179
                                return nil, err
2✔
180
                        }
2✔
181

182
                        result.Query = append(result.Query, term)
8✔
183
                }
184
        }
185

186
        if result.Query == nil {
14✔
187
                return nil, nil
4✔
188
        }
4✔
189

190
        return result, nil
6✔
191
}
192

193
// ////////////////////////////////////////////////////////////////////////////////// //
194

195
// parseTerm parses query term
196
func parseTerm(rawTerm string) (*search.Term, error) {
122✔
197
        name, value, isNegative := extractTermInfo(rawTerm)
122✔
198
        termType, mod := terms[name], uint8(0)
122✔
199

122✔
200
        if name != "" {
242✔
201
                if termType == search.TERM_UNKNOWN {
124✔
202
                        return nil, fmt.Errorf("Unknown query term %q", name)
4✔
203
                }
4✔
204
        } else {
2✔
205
                termType = 255 // term without name = name prefix search
2✔
206
        }
2✔
207

208
        if isNegative {
120✔
209
                mod = search.TERM_MOD_NEGATIVE
2✔
210
        }
2✔
211

212
        switch termType {
118✔
213
        case search.TERM_NAME:
14✔
214
                return search.TermName(value, mod), nil
14✔
215
        case search.TERM_VERSION:
4✔
216
                return search.TermVersion(value, mod), nil
4✔
217
        case search.TERM_RELEASE:
4✔
218
                return search.TermRelease(value, mod), nil
4✔
219
        case search.TERM_EPOCH:
4✔
220
                return search.TermEpoch(value, mod), nil
4✔
221
        case search.TERM_ARCH:
4✔
222
                return search.TermArch(formatArchValue(value), mod), nil
4✔
223
        case search.TERM_REQUIRES:
4✔
224
                return parseDepTermValue(search.TERM_REQUIRES, value, mod)
4✔
225
        case search.TERM_PROVIDES:
4✔
226
                return parseDepTermValue(search.TERM_PROVIDES, value, mod)
4✔
227
        case search.TERM_RECOMMENDS:
4✔
228
                return parseDepTermValue(search.TERM_RECOMMENDS, value, mod)
4✔
229
        case search.TERM_CONFLICTS:
4✔
230
                return parseDepTermValue(search.TERM_CONFLICTS, value, mod)
4✔
231
        case search.TERM_OBSOLETES:
4✔
232
                return parseDepTermValue(search.TERM_OBSOLETES, value, mod)
4✔
233
        case search.TERM_ENHANCES:
4✔
234
                return parseDepTermValue(search.TERM_ENHANCES, value, mod)
4✔
235
        case search.TERM_SUGGESTS:
4✔
236
                return parseDepTermValue(search.TERM_SUGGESTS, value, mod)
4✔
237
        case search.TERM_SUPPLEMENTS:
4✔
238
                return parseDepTermValue(search.TERM_SUPPLEMENTS, value, mod)
4✔
239
        case search.TERM_FILE:
4✔
240
                return search.TermFile(value, mod), nil
4✔
241
        case search.TERM_SOURCE:
2✔
242
                return search.TermSource(value, mod), nil
2✔
243
        case search.TERM_LICENSE:
4✔
244
                return search.TermLicense(value, mod), nil
4✔
245
        case search.TERM_VENDOR:
4✔
246
                return search.TermVendor(value, mod), nil
4✔
247
        case search.TERM_GROUP:
4✔
248
                return search.TermGroup(value, mod), nil
4✔
249
        case search.TERM_BUILD_HOST:
4✔
250
                return search.TermBuildHost(value, mod), nil
4✔
251
        case search.TERM_DATE_ADD, search.TERM_DATE_BUILD:
14✔
252
                return parseDateTermValue(termType, value, mod)
14✔
253
        case search.TERM_SIZE:
14✔
254
                return parseSizeTermValue(value, mod)
14✔
255
        case search.TERM_PAYLOAD:
4✔
256
                return search.TermPayload(value, mod), nil
4✔
257
        default:
2✔
258
                return search.TermName(value+"*", mod), nil
2✔
259
        }
260
}
261

262
// parseExtTerm parses extended (not supported by search package) terms
263
func parseExtTerm(rawTerm string, searchResult *Request) error {
6✔
264
        name, value, isNegative := extractTermInfo(rawTerm)
6✔
265

6✔
266
        if name == TERM_RELEASED || name == TERM_SHORT_RELEASED {
12✔
267
                v, err := parseBoolTermValue(value, isNegative)
6✔
268

6✔
269
                if err != nil {
8✔
270
                        return err
2✔
271
                }
2✔
272

273
                if v {
6✔
274
                        searchResult.FilterFlag = FILTER_FLAG_RELEASED
2✔
275
                } else {
4✔
276
                        searchResult.FilterFlag = FILTER_FLAG_UNRELEASED
2✔
277
                }
2✔
278
        }
279

280
        return nil
4✔
281
}
282

283
// parseDateTermValue parses date term value
284
func parseDateTermValue(termType uint8, value string, mod uint8) (*search.Term, error) {
14✔
285
        dur, err := timeutil.ParseDuration(value, 'd')
14✔
286

14✔
287
        if err != nil {
16✔
288
                return nil, fmt.Errorf("Can't parse %q as duration: %v", value, err)
2✔
289
        }
2✔
290

291
        now := time.Now()
12✔
292
        to := now.Unix()
12✔
293
        from := now.Add(-1 * dur).Unix()
12✔
294

12✔
295
        return &search.Term{Type: termType, Value: search.Range{from, to}, Modificator: mod}, nil
12✔
296
}
297

298
// parseBoolTermValue parses boolean term value
299
func parseBoolTermValue(value string, isNegative bool) (bool, error) {
16✔
300
        var result bool
16✔
301

16✔
302
        switch strings.ToLower(value) {
16✔
303
        case "":
2✔
304
                return false, fmt.Errorf("Query term value can not be empty")
2✔
305
        case "yes", "y", "true", "1":
6✔
306
                result = true
6✔
307
        case "no", "n", "false", "0":
4✔
308
                result = false
4✔
309
        default:
4✔
310
                return false, fmt.Errorf("Unsupported query term value %q", value)
4✔
311
        }
312

313
        if isNegative {
12✔
314
                return !result, nil
2✔
315
        }
2✔
316

317
        return result, nil
8✔
318
}
319

320
// parseSizeTermValue parses size term value
321
func parseSizeTermValue(value string, mod uint8) (*search.Term, error) {
14✔
322
        var errFrom, errTo, errSize error
14✔
323
        var from, to, size uint64
14✔
324

14✔
325
        switch {
14✔
326
        case strings.HasSuffix(value, "-"):
2✔
327
                from = 0
2✔
328
                to, errTo = fmtutil.ParseSize(strings.TrimRight(value, "-"))
2✔
329

330
        case strings.HasSuffix(value, "+"):
2✔
331
                from, errFrom = fmtutil.ParseSize(strings.TrimRight(value, "+"))
2✔
332
                to = 1024 * 1024 * 1024
2✔
333

334
        case strings.Contains(value, "-"):
4✔
335
                from, errFrom = fmtutil.ParseSize(strutil.ReadField(value, 0, false, '-'))
4✔
336
                to, errTo = fmtutil.ParseSize(strutil.ReadField(value, 1, false, '-'))
4✔
337

338
        default:
6✔
339
                size, errSize = fmtutil.ParseSize(value)
6✔
340
                diff := uint64(float64(size) * 0.2)
6✔
341
                from = mathutil.Between(size-diff, 0, 1024*1024*1024)
6✔
342
                to = mathutil.Between(size+diff, 0, 1024*1024*1024)
6✔
343
        }
344

345
        switch {
14✔
346
        case errSize != nil:
×
347
                return nil, fmt.Errorf("Invalid size value: %w", errSize)
×
348
        case errFrom != nil:
×
349
                return nil, fmt.Errorf("Invalid range start: %w", errFrom)
×
350
        case errTo != nil:
×
351
                return nil, fmt.Errorf("Invalid range end: %w", errTo)
×
352
        case from > to:
2✔
353
                return nil, fmt.Errorf("Range %d→%d is invalid", from, to)
2✔
354
        }
355

356
        return search.TermSize(int64(from), int64(to), mod), nil
12✔
357
}
358

359
// parseDepTermValue parses term with dependency info (used for requires/provides)
360
func parseDepTermValue(termType uint8, value string, mod uint8) (*search.Term, error) {
34✔
361
        dep := extractDepInfo(value)
34✔
362

34✔
363
        if dep.Flag != data.COMP_FLAG_ANY {
36✔
364
                if dep.Epoch == "" && dep.Version == "" && dep.Release == "" {
4✔
365
                        return nil, fmt.Errorf("Can't use %q - condition without value", value)
2✔
366
                }
2✔
367
        }
368

369
        return &search.Term{Type: termType, Value: dep, Modificator: mod}, nil
32✔
370
}
371

372
// extractDepInfo parses and extracts dependency info
373
func extractDepInfo(v string) data.Dependency {
42✔
374
        info := depRegex.FindStringSubmatch(v)
42✔
375

42✔
376
        return data.Dependency{
42✔
377
                Name:    info[1],
42✔
378
                Epoch:   strings.TrimRight(info[3], ":"),
42✔
379
                Version: info[4],
42✔
380
                Release: info[5],
42✔
381
                Flag:    condToFlag(info[2]),
42✔
382
        }
42✔
383
}
42✔
384

385
// extractTermInfo extracts info from token
386
func extractTermInfo(rawTerm string) (string, string, bool) {
144✔
387
        if !strings.Contains(rawTerm, ":") {
146✔
388
                return "", rawTerm, false
2✔
389
        }
2✔
390

391
        name, value, _ := strings.Cut(rawTerm, ":")
142✔
392
        isNegative := false
142✔
393

142✔
394
        if strings.HasPrefix(value, ":") {
144✔
395
                isNegative = true
2✔
396
                value = value[1:]
2✔
397
        }
2✔
398

399
        return name, value, isNegative
142✔
400
}
401

402
// formatArchValue formats arch term value and converts tags into
403
// full arch name
404
func formatArchValue(arch string) string {
14✔
405
        arch = strings.ToLower(arch)
14✔
406

14✔
407
        for i := len(data.ArchList) - 1; i > 0; i-- {
154✔
408
                archName := data.ArchList[i]
140✔
409
                archInfo := data.SupportedArchs[archName]
140✔
410

140✔
411
                if strings.Contains(arch, archInfo.Tag) && archInfo.Dir != "" {
144✔
412
                        arch = strings.ReplaceAll(arch, archInfo.Tag, archName)
4✔
413
                }
4✔
414
        }
415

416
        return arch
14✔
417
}
418

419
// condToFlag transforms conditional to flag
420
func condToFlag(c string) data.CompFlag {
54✔
421
        switch c {
54✔
422
        case ">=":
6✔
423
                return data.COMP_FLAG_GE
6✔
424
        case "<=":
2✔
425
                return data.COMP_FLAG_LE
2✔
426
        case ">":
4✔
427
                return data.COMP_FLAG_GT
4✔
428
        case "<":
2✔
429
                return data.COMP_FLAG_LT
2✔
430
        case "=":
4✔
431
                return data.COMP_FLAG_EQ
4✔
432
        default:
36✔
433
                return data.COMP_FLAG_ANY
36✔
434
        }
435
}
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