diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-06-20 09:40:07 -0700 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-06-20 09:40:07 -0700 |
| commit | fb653292612eddca5b088ed88466c546d1597107 (patch) | |
| tree | 900ed32e066812688858ed3bb15128c41bb78054 /utils | |
| download | penguins.lan-fb653292612eddca5b088ed88466c546d1597107.tar.gz penguins.lan-fb653292612eddca5b088ed88466c546d1597107.zip | |
Initial commit
Diffstat (limited to 'utils')
| -rw-r--r-- | utils/net.go | 59 | ||||
| -rw-r--r-- | utils/net_test.go | 63 | ||||
| -rw-r--r-- | utils/random_id.go | 15 | ||||
| -rw-r--r-- | utils/text.go | 49 |
4 files changed, 186 insertions, 0 deletions
diff --git a/utils/net.go b/utils/net.go new file mode 100644 index 0000000..76454bd --- /dev/null +++ b/utils/net.go @@ -0,0 +1,59 @@ +package utils + +import ( + "fmt" + "strings" +) + +// Mac is a 48-bit hardware address. +type Mac struct { + Addr [6]byte +} + +// Format renders the canonical lowercase, zero-padded form aa:bb:cc:dd:ee:ff. +func (m *Mac) Format() string { + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", + m.Addr[0], m.Addr[1], m.Addr[2], m.Addr[3], m.Addr[4], m.Addr[5]) +} + +// ParseMac reads a colon-separated hex MAC. It tolerates uppercase and the +// zero-stripped octets macOS prints (e.g. "8:0:27:ab:cd:ef"). +func ParseMac(s string) (*Mac, error) { + mac := &Mac{} + t := strings.TrimSpace(s) + n, err := fmt.Sscanf(t, "%x:%x:%x:%x:%x:%x", + &mac.Addr[0], &mac.Addr[1], &mac.Addr[2], &mac.Addr[3], &mac.Addr[4], &mac.Addr[5]) + if err != nil { + return nil, err + } + if n != 6 { + return nil, fmt.Errorf("invalid mac %q: parsed %d of 6 octets", s, n) + } + return mac, nil +} + +// INet4 is an IPv4 address. +type INet4 struct { + Addr [4]byte +} + +// Format renders the dotted-decimal form a.b.c.d. +func (n *INet4) Format() string { + return fmt.Sprintf("%d.%d.%d.%d", n.Addr[0], n.Addr[1], n.Addr[2], n.Addr[3]) +} + +// ParseINet4 reads a dotted-decimal IPv4 address. Out-of-range octets (>255) +// overflow the byte and are rejected. +func ParseINet4(s string) (*INet4, error) { + addr := &INet4{} + t := strings.TrimSpace(s) + n, err := fmt.Sscanf(t, "%d.%d.%d.%d", + &addr.Addr[0], &addr.Addr[1], &addr.Addr[2], &addr.Addr[3]) + if err != nil { + return nil, err + } + if n != 4 { + return nil, fmt.Errorf("invalid ipv4 %q: parsed %d of 4 octets", s, n) + } + return addr, nil +} diff --git a/utils/net_test.go b/utils/net_test.go new file mode 100644 index 0000000..27d991d --- /dev/null +++ b/utils/net_test.go @@ -0,0 +1,63 @@ +package utils + +import "testing" + +func TestParseMacAndFormat(t *testing.T) { + // each input should parse and round-trip to the canonical form + canonical := map[string]string{ + "aa:bb:cc:dd:ee:ff": "aa:bb:cc:dd:ee:ff", // already canonical + "AA:BB:CC:DD:EE:FF": "aa:bb:cc:dd:ee:ff", // uppercase + "8:0:27:ab:cd:ef": "08:00:27:ab:cd:ef", // macOS zero-stripped octets + " de:ad:be:ef:00:01 ": "de:ad:be:ef:00:01", // surrounding space + } + for in, want := range canonical { + mac, err := ParseMac(in) + if err != nil { + t.Errorf("ParseMac(%q) unexpected error: %v", in, err) + continue + } + if got := mac.Format(); got != want { + t.Errorf("ParseMac(%q).Format() = %q, want %q", in, got, want) + } + } +} + +func TestParseMacInvalid(t *testing.T) { + for _, in := range []string{ + "", // empty + "aa:bb:cc:dd:ee", // too few octets + "hello", // not a mac + "zz:zz:zz:zz:zz:zz", // non-hex digits + } { + if _, err := ParseMac(in); err == nil { + t.Errorf("ParseMac(%q) = nil error, want error", in) + } + } +} + +func TestParseINet4AndFormat(t *testing.T) { + for _, ip := range []string{"192.168.1.5", "10.0.0.2", "0.0.0.0", "255.255.255.255"} { + addr, err := ParseINet4(ip) + if err != nil { + t.Errorf("ParseINet4(%q) unexpected error: %v", ip, err) + continue + } + if got := addr.Format(); got != ip { + t.Errorf("ParseINet4(%q).Format() = %q, want %q", ip, got, ip) + } + } +} + +func TestParseINet4Invalid(t *testing.T) { + for _, in := range []string{ + "", // empty + "1.2.3", // too few octets + "192.168.1.256", // octet overflows a byte + "::1", // IPv6, not dotted-decimal + "hello", // garbage + } { + if _, err := ParseINet4(in); err == nil { + t.Errorf("ParseINet4(%q) = nil error, want error", in) + } + } +} diff --git a/utils/random_id.go b/utils/random_id.go new file mode 100644 index 0000000..e6dc731 --- /dev/null +++ b/utils/random_id.go @@ -0,0 +1,15 @@ +package utils + +import ( + "crypto/rand" + "fmt" +) + +// RandomId returns a 128-bit hex id, handy for sessions, db rows, etc. +func RandomId() string { + id := make([]byte, 16) + if _, err := rand.Read(id); err != nil { + panic(err) + } + return fmt.Sprintf("%x", id) +} diff --git a/utils/text.go b/utils/text.go new file mode 100644 index 0000000..5a79ec9 --- /dev/null +++ b/utils/text.go @@ -0,0 +1,49 @@ +package utils + +import ( + "fmt" + "math/rand" + "strings" + "time" +) + +var nameAdjectives = []string{"waddly", "frosty", "noot", "sleepy", "tuxedo", "briny", "squeaky", "blizzard", "pebble", "slippy"} +var nameNouns = []string{"penguin", "gentoo", "rockhopper", "emperor", "macaroni", "chinstrap", "skua", "krill", "iceberg", "fishstick"} + +func SuggestName() string { + return nameAdjectives[rand.Intn(len(nameAdjectives))] + "-" + nameNouns[rand.Intn(len(nameNouns))] +} + +func Sanitize(raw string) string { + return strings.ReplaceAll(strings.TrimSpace(raw), "\n", " ") +} + +func SanitizeName(raw string) string { + clean := strings.ToLower(Sanitize(raw)) + clean = strings.ReplaceAll(clean, " ", "-") + if len(clean) > 24 { + clean = clean[:24] + } + return clean +} + +func FormatSince(t time.Time) string { + d := time.Since(t) + switch { + case d < time.Minute && d.Seconds() < 20: + return "just now" + case d < time.Hour: + return fmt.Sprintf("%d minutes ago", int(d.Minutes())) + case d < 24*time.Hour: + return fmt.Sprintf("%d hours ago", int(d.Hours())) + default: + return fmt.Sprintf("%d days ago", int(d.Hours()/24)) + } +} + +func Truncate(s string, n int) string { + if len(s) > n { + return s[:n] + } + return s +} |
