repos / dbin

📦 Poor man's package manager.
git clone https://github.com/xplshn/dbin.git

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}