xplshn
·
2025-08-13
utility.go
Go
1package main
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "net/http"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "time"
13
14 _ "github.com/breml/rootcerts" // built-in ca certs
15
16 "github.com/fxamacker/cbor/v2" //"github.com/shamaton/msgpack/v2"
17 "github.com/goccy/go-json"
18 "github.com/goccy/go-yaml"
19 "golang.org/x/term"
20
21 "github.com/klauspost/compress/gzip"
22 "github.com/klauspost/compress/zstd"
23
24 "github.com/pkg/xattr"
25 "github.com/zeebo/blake3"
26 "github.com/zeebo/errs"
27)
28
29var (
30 errFileAccess = errs.Class("file access error")
31 errFileTypeInvalid = errs.Class("invalid file type")
32 errFileNotFound = errs.Class("file not found")
33 errXAttr = errs.Class("xattr error")
34 errCacheAccess = errs.Class("cache access error")
35 errNoURLs = errs.Class("no URLs provided")
36 delimiters = []rune{
37 '#', // .PkgID
38 ':', // .Version
39 '@', // .Repository.Name
40 }
41)
42
43const (
44 blueColor = "\x1b[0;34m"
45 yellowColor = "\x1b[0;33m"
46 cyanColor = "\x1b[0;36m"
47 intenseBlackColor = "\x1b[0;90m"
48 blueBgWhiteFg = "\x1b[48;5;4m"
49 resetColor = "\x1b[0m"
50)
51
52func fileExists(filePath string) bool {
53 _, err := os.Stat(filePath)
54 return !os.IsNotExist(err)
55}
56
57func isExecutable(filePath string) bool {
58 info, err := os.Stat(filePath)
59 if err != nil {
60 return false
61 }
62 return info.Mode().IsRegular() && (info.Mode().Perm()&0o111) != 0
63}
64
65func parseBinaryEntry(entry binaryEntry, ansi bool) string {
66 result := entry.Name
67
68 if ansi && term.IsTerminal(int(os.Stdout.Fd())) {
69 if entry.PkgID != "" {
70 result += blueColor + string(delimiters[0]) + entry.PkgID + resetColor
71 }
72 if entry.Version != "" {
73 result += cyanColor + string(delimiters[1]) + entry.Version + resetColor
74 }
75 if entry.Repository.Name != "" {
76 result += intenseBlackColor + string(delimiters[2]) + entry.Repository.Name + resetColor
77 }
78 return result
79 }
80
81 if entry.PkgID != "" {
82 result += string(delimiters[0]) + entry.PkgID
83 }
84 //if entry.Version != "" {
85 // result += string(delimiters[1]) + entry.Version
86 //}
87 if entry.Repository.Name != "" {
88 result += string(delimiters[2]) + entry.Repository.Name
89 }
90 return result
91}
92
93func stringToBinaryEntry(input string) binaryEntry {
94 var bEntry binaryEntry
95
96 // Accepted formats:
97 //
98 // oci://* || http*://* [
99 // .Name = basename of url
100 // .Bsum = "!no_check"
101 // ]
102 //
103 // name#id:version@repo
104 // name#id:version
105 // name#id@repo
106 // name#id
107 // name@repo
108 // name
109
110 // Lazily Check for URI
111 if idx := strings.Index(input, "://"); idx >= 0 && idx <= 8 {
112 bEntry.Name = filepath.Base(input)
113 bEntry.Bsum = "!no_check"
114 bEntry.DownloadURL = input
115 return bEntry
116 }
117
118 // Split by repository delimiter (@)
119 parts := strings.SplitN(input, string(delimiters[2]), 2)
120 bEntry.Name = parts[0]
121 if len(parts) > 1 {
122 bEntry.Repository.Name = parts[1]
123 }
124
125 // Split name part by ID delimiter (#)
126 nameParts := strings.SplitN(bEntry.Name, string(delimiters[0]), 2)
127 bEntry.Name = nameParts[0]
128 if len(nameParts) > 1 {
129 // Split ID part by version delimiter (:)
130 idVer := strings.SplitN(nameParts[1], string(delimiters[1]), 2)
131 bEntry.PkgID = idVer[0]
132 if len(idVer) > 1 {
133 bEntry.Version = idVer[1]
134 }
135 }
136
137 return bEntry
138}
139
140func arrStringToArrBinaryEntry(args []string) []binaryEntry {
141 var entries []binaryEntry
142 for _, arg := range args {
143 entries = append(entries, stringToBinaryEntry(arg))
144 }
145 return entries
146}
147
148func binaryEntriesToArrString(entries []binaryEntry, ansi bool) []string {
149 var result []string
150 seen := make(map[string]bool)
151
152 for _, entry := range entries {
153 key := parseBinaryEntry(entry, ansi)
154 if !seen[key] {
155 result = append(result, key)
156 } else {
157 seen[key] = true
158 if entry.Version != "" {
159 result = append(result, key, ternary(!ansi, entry.Version, "\033[90m"+entry.Version+"\033[0m"))
160 }
161 }
162 }
163
164 return result
165}
166
167func validateProgramsFrom(config *config, programsToValidate []binaryEntry, uRepoIndex []binaryEntry) ([]binaryEntry, error) {
168 var (
169 programsEntries []binaryEntry
170 validPrograms []binaryEntry
171 err error
172 files []string
173 )
174
175 if config.RetakeOwnership {
176 if uRepoIndex == nil {
177 uRepoIndex, err = fetchRepoIndex(config)
178 if err != nil {
179 return nil, err
180 }
181 }
182
183 programsEntries, err = listBinaries(uRepoIndex)
184 if err != nil {
185 return nil, fmt.Errorf("failed to list remote binaries: %w", err)
186 }
187 }
188
189 files, err = listFilesInDir(config.InstallDir)
190 if err != nil {
191 return nil, fmt.Errorf("failed to list files in %s: %w", config.InstallDir, err)
192 }
193
194 var toProcess []string
195 if len(programsToValidate) == 0 {
196 // All files in install dir
197 toProcess = files
198 } else {
199 // Only the specific binaries requested
200 toProcess = toProcess[:0]
201 for i := range programsToValidate {
202 file := filepath.Join(config.InstallDir, programsToValidate[i].Name)
203 toProcess = append(toProcess, file)
204 }
205 }
206
207 // Only allocate once, at most as many entries as files to process
208 validPrograms = make([]binaryEntry, 0, len(toProcess))
209
210 for i := range toProcess {
211 file := toProcess[i]
212 if !isExecutable(file) || (len(programsToValidate) != 0 && !fileExists(file)) {
213 continue
214 }
215
216 baseName := filepath.Base(file)
217 trackedBEntry := bEntryOfinstalledBinary(file)
218
219 if config.RetakeOwnership {
220 if trackedBEntry.Name == "" {
221 trackedBEntry.Name = baseName
222 trackedBEntry.PkgID = "!retake"
223 }
224
225 for j := range programsEntries {
226 if programsEntries[j].Name == trackedBEntry.Name {
227 validPrograms = append(validPrograms, trackedBEntry)
228 break
229 }
230 }
231 continue
232 }
233
234 // Non-retake: must have metadata and match uRepoIndex
235 if trackedBEntry.Name == "" {
236 continue
237 }
238 if uRepoIndex == nil {
239 // If uRepoIndex is nil, append any entry with Name != ""
240 validPrograms = append(validPrograms, trackedBEntry)
241 continue
242 }
243 for j := range uRepoIndex {
244 if uRepoIndex[j].Name == trackedBEntry.Name && uRepoIndex[j].PkgID == trackedBEntry.PkgID {
245 validPrograms = append(validPrograms, trackedBEntry)
246 break
247 }
248 }
249 }
250
251 return validPrograms, nil
252}
253
254func bEntryOfinstalledBinary(binaryPath string) binaryEntry {
255 if isSymlink(binaryPath) {
256 return binaryEntry{}
257 }
258 trackedBEntry, err := readEmbeddedBEntry(binaryPath)
259 if err != nil || trackedBEntry.Name == "" {
260 return binaryEntry{}
261 }
262 return trackedBEntry
263}
264
265func getTerminalWidth() int {
266 w, _, _ := term.GetSize(int(os.Stdout.Fd()))
267 if w != 0 {
268 return w
269 }
270 return 80
271}
272
273func truncateSprintf(indicator, format string, a ...any) string {
274 text := fmt.Sprintf(format, a...)
275 if !term.IsTerminal(int(os.Stdout.Fd())) {
276 return text
277 }
278
279 width := uint(getTerminalWidth() - len(indicator))
280 if width <= 0 {
281 return text
282 }
283
284 var out bytes.Buffer
285 var visibleCount uint
286 var inEscape bool
287 var escBuf bytes.Buffer
288
289 for i := range len(text) {
290 c := text[i]
291
292 switch {
293 case c == '\x1b':
294 inEscape = true
295 escBuf.Reset()
296 escBuf.WriteByte(c)
297 case inEscape:
298 escBuf.WriteByte(c)
299 if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
300 inEscape = false
301 out.Write(escBuf.Bytes())
302 }
303 default:
304 if visibleCount >= width {
305 continue
306 }
307 out.WriteByte(c)
308 visibleCount++
309 }
310 }
311
312 result := out.String()
313 if strings.HasSuffix(text, "\n") {
314 if visibleCount >= width {
315 return result + indicator + "\n"
316 }
317 return result
318 }
319 if visibleCount >= width {
320 return result + indicator
321 }
322 return result
323}
324
325func truncatePrintf(disableTruncation bool, format string, a ...any) (n int, err error) {
326 if disableTruncation {
327 return fmt.Printf(format, a...)
328 }
329 text := truncateSprintf("..>", format, a...)
330 return fmt.Print(text)
331}
332
333func listFilesInDir(dir string) ([]string, error) {
334 entries, err := os.ReadDir(dir)
335 if err != nil {
336 return nil, errFileAccess.Wrap(err)
337 }
338 files := make([]string, 0, len(entries))
339 for _, entry := range entries {
340 if !entry.IsDir() {
341 files = append(files, filepath.Join(dir, entry.Name()))
342 }
343 }
344 return files, nil
345}
346
347func embedBEntry(binaryPath string, bEntry binaryEntry) error {
348 bEntry.Version = ""
349 if err := xattr.Set(binaryPath, "user.FullName", []byte(parseBinaryEntry(bEntry, false))); err != nil {
350 return errXAttr.Wrap(err)
351 }
352 return nil
353}
354
355func readEmbeddedBEntry(binaryPath string) (binaryEntry, error) {
356 if !fileExists(binaryPath) {
357 return binaryEntry{}, errFileNotFound.New("Tried to get EmbeddedBEntry of non-existent file: %s", binaryPath)
358 }
359
360 fullName, err := xattr.Get(binaryPath, "user.FullName")
361 if err != nil {
362 return binaryEntry{}, errXAttr.New("xattr: user.FullName attribute not found for binary: %s", binaryPath)
363 }
364
365 bEntry := stringToBinaryEntry(string(fullName))
366 bEntry.binaryPath = binaryPath
367
368 return bEntry, nil
369}
370
371func accessCachedOrFetch(urls []string, filename string, cfg *config, syncInterval time.Duration) ([]byte, error) {
372 if len(urls) == 0 {
373 return nil, errNoURLs.Wrap(errs.New("urls: []string contains no URLs"))
374 }
375 mainURL := urls[0]
376 var fallbacks []string
377 if len(urls) > 1 {
378 fallbacks = urls[1:]
379 }
380
381 cacheFilePath := filepath.Join(cfg.CacheDir, ternary(filename != "", "."+filename, "."+filepath.Base(mainURL)))
382
383 if err := os.MkdirAll(cfg.CacheDir, 0755); err != nil {
384 return nil, errCacheAccess.Wrap(err)
385 }
386
387 if info, err := os.Stat(cacheFilePath); err == nil && time.Since(info.ModTime()) < syncInterval {
388 data, err := os.ReadFile(cacheFilePath)
389 if err != nil {
390 return nil, errCacheAccess.Wrap(err)
391 }
392 return data, nil
393 }
394
395 tryFetch := func(u string) ([]byte, int, error) {
396 req, err := http.NewRequest("GET", u, nil)
397 if err != nil {
398 return nil, -1, err
399 }
400 req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
401 req.Header.Set("Pragma", "no-cache")
402 req.Header.Set("dbin", strconv.FormatFloat(version, 'f', -1, 32))
403
404 resp, err := http.DefaultClient.Do(req)
405 if err != nil {
406 return nil, -1, err
407 }
408 defer resp.Body.Close()
409
410 body, err := io.ReadAll(resp.Body)
411 return body, resp.StatusCode, err
412 }
413
414 // Try main
415 body, code, err := tryFetch(mainURL)
416 if err == nil && code == http.StatusOK {
417 _ = os.WriteFile(cacheFilePath, body, 0644)
418 return body, nil
419 }
420 if err != nil {
421 fmt.Fprintf(os.Stderr, "Warning: [nil] Failed to fetch: %s\n", mainURL)
422 } else if code != http.StatusTooManyRequests {
423 fmt.Fprintf(os.Stderr, "Warning: [%d] Failed to fetch: %s\n", code, mainURL)
424 }
425
426 // Try fallbacks
427 for i, fb := range fallbacks {
428 body, code, err := tryFetch(fb)
429 if err != nil {
430 fmt.Fprintf(os.Stderr, "Warning: [net] Fallback[%d] failed: %s — %v\n", i, fb, err)
431 continue
432 }
433 if code == http.StatusOK {
434 _ = os.WriteFile(cacheFilePath, body, 0644)
435 return body, nil
436 }
437 fmt.Fprintf(os.Stderr, "Warning: [%d] Fallback[%d] failed: %s\n", code, i, fb)
438 }
439
440 return nil, errCacheAccess.New("fetch failed for %s", mainURL)
441}
442
443func decodeRepoIndex(config *config) ([]binaryEntry, error) {
444 var binaryEntries []binaryEntry
445 var parsedRepos = make(map[string]bool)
446
447 for _, repo := range config.Repositories {
448 if parsedRepos[repo.URL] {
449 continue
450 }
451
452 var bodyBytes []byte
453 var err error
454
455 if strings.HasPrefix(repo.URL, "file://") {
456 bodyBytes, err = os.ReadFile(strings.TrimPrefix(repo.URL, "file://"))
457 if err != nil {
458 return nil, errFileAccess.Wrap(err)
459 }
460 } else {
461 urls := append([]string{repo.URL}, repo.FallbackURLs...)
462 bodyBytes, err = accessCachedOrFetch(urls, "", config, repo.SyncInterval)
463 if err != nil {
464 return nil, err
465 }
466 }
467
468 bodyReader := io.NopCloser(bytes.NewReader(bodyBytes))
469
470 switch {
471 case strings.HasSuffix(repo.URL, ".gz"):
472 repo.URL = strings.TrimSuffix(repo.URL, ".gz")
473 gzipReader, err := gzip.NewReader(bodyReader)
474 if err != nil {
475 return nil, errFileTypeInvalid.Wrap(err)
476 }
477 defer gzipReader.Close()
478
479 bodyBytes, err = io.ReadAll(gzipReader)
480 if err != nil {
481 return nil, errFileAccess.Wrap(err)
482 }
483 case strings.HasSuffix(repo.URL, ".zst"):
484 repo.URL = strings.TrimSuffix(repo.URL, ".zst")
485 zstdReader, err := zstd.NewReader(bodyReader)
486 if err != nil {
487 return nil, errFileTypeInvalid.Wrap(err)
488 }
489 defer zstdReader.Close()
490
491 bodyBytes, err = io.ReadAll(zstdReader.IOReadCloser())
492 if err != nil {
493 return nil, errFileAccess.Wrap(err)
494 }
495 }
496
497 var repoIndex map[string][]binaryEntry
498 switch {
499 //case strings.HasSuffix(repo.URL, ".msgp"):
500 // if err := msgpack.Unmarshal(bodyBytes, &repoIndex); err != nil {
501 // return nil, errFileTypeInvalid.Wrap(err)
502 // }
503 case strings.HasSuffix(repo.URL, ".cbor"):
504 if err := cbor.Unmarshal(bodyBytes, &repoIndex); err != nil {
505 return nil, errFileTypeInvalid.Wrap(err)
506 }
507 case strings.HasSuffix(repo.URL, ".json"):
508 if err := json.Unmarshal(bodyBytes, &repoIndex); err != nil {
509 return nil, errFileTypeInvalid.Wrap(err)
510 }
511 case strings.HasSuffix(repo.URL, ".yaml"):
512 if err := yaml.Unmarshal(bodyBytes, &repoIndex); err != nil {
513 return nil, errFileTypeInvalid.Wrap(err)
514 }
515 default:
516 return nil, errFileTypeInvalid.New("unsupported format for URL: %s", repo.URL)
517 }
518
519 for repoName, entries := range repoIndex {
520 for _, entry := range entries {
521 entry.Repository = repo
522 entry.Repository.Name = repoName
523 binaryEntries = append(binaryEntries, entry)
524 }
525 }
526
527 parsedRepos[repo.URL] = true
528 }
529
530 return binaryEntries, nil
531}
532
533func calculateChecksum(filePath string) (string, error) {
534 file, err := os.Open(filePath)
535 if err != nil {
536 return "", errFileAccess.Wrap(err)
537 }
538 defer file.Close()
539
540 hasher := blake3.New()
541 if _, err := io.Copy(hasher, file); err != nil {
542 return "", errFileAccess.Wrap(err)
543 }
544
545 return fmt.Sprintf("%x", hasher.Sum(nil)), nil
546}
547
548func isSymlink(filePath string) bool {
549 fileInfo, err := os.Lstat(filePath)
550 return err == nil && fileInfo.Mode()&os.ModeSymlink != 0
551}
552
553func ternary[T any](cond bool, vtrue, vfalse T) T {
554 if cond {
555 return vtrue
556 }
557 return vfalse
558}