Major refactoring.

+ Many lib functions have been split out into separate packages.
+ Adding cert/key generation tooling.
+ Add new time.Duration parser.
This commit is contained in:
2025-11-19 01:34:26 -08:00
parent 90a48a1890
commit 154d5a6c2e
20 changed files with 459 additions and 36 deletions

View File

@@ -12,7 +12,7 @@
// 3. HTTP_PROXY
//
// Both uppercase and lowercase variable names are honored.
package lib
package dialer
import (
"bufio"
@@ -468,8 +468,8 @@ func (s *socks5ContextDialer) DialContext(ctx context.Context, network, address
// tlsWrappingDialer performs a TLS handshake over an existing base dialer.
type tlsWrappingDialer struct {
base ContextDialer
tcfg *tls.Config
base ContextDialer
tcfg *tls.Config
timeout time.Duration
}

1
lib/duration/duration.go Normal file
View File

@@ -0,0 +1 @@
package duration

View File

@@ -1,4 +1,4 @@
package lib
package fetch
import (
"context"
@@ -12,6 +12,8 @@ import (
"git.wntrmute.dev/kyle/goutils/certlib"
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
"git.wntrmute.dev/kyle/goutils/fileutil"
"git.wntrmute.dev/kyle/goutils/lib"
"git.wntrmute.dev/kyle/goutils/lib/dialer"
)
// Note: Previously this package exposed a FetcherOpts type. It has been
@@ -61,18 +63,18 @@ func ParseServer(host string) (*ServerFetcher, error) {
}
func (sf *ServerFetcher) String() string {
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, Itoa(sf.port, -1)))
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)))
}
func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) {
opts := DialerOpts{
opts := dialer.DialerOpts{
TLSConfig: &tls.Config{
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
RootCAs: sf.roots,
},
}
conn, err := DialTLS(context.Background(), net.JoinHostPort(sf.host, Itoa(sf.port, -1)), opts)
conn, err := dialer.DialTLS(context.Background(), net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)), opts)
if err != nil {
return nil, fmt.Errorf("failed to dial server: %w", err)
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
@@ -112,6 +113,85 @@ func Duration(d time.Duration) string {
return s
}
// IsDigit checks if a byte is a decimal digit.
func IsDigit(b byte) bool {
return b >= '0' && b <= '9'
}
// ParseDuration parses a duration string into a time.Duration.
// It supports standard units (ns, us/µs, ms, s, m, h) plus extended units:
// d (days, 24h), w (weeks, 7d), y (years, 365d).
// Units can be combined without spaces, e.g., "1y2w3d4h5m6s".
// Case-insensitive. Years and days are approximations (no leap seconds/months).
// Returns an error for invalid input.
func ParseDuration(s string) (time.Duration, error) {
s = strings.ToLower(s) // Normalize to lowercase for case-insensitivity.
if s == "" {
return 0, fmt.Errorf("empty duration string")
}
var total time.Duration
i := 0
for i < len(s) {
// Parse the number part.
start := i
for i < len(s) && IsDigit(s[i]) {
i++
}
if start == i {
return 0, fmt.Errorf("expected number at position %d", start)
}
numStr := s[start:i]
num, err := strconv.ParseUint(numStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid number %q: %w", numStr, err)
}
// Parse the unit part.
if i >= len(s) {
return 0, fmt.Errorf("expected unit after number %q", numStr)
}
unitStart := i
i++ // Consume the first char of the unit.
unit := s[unitStart:i]
// Handle potential two-char units like "ms".
if unit == "m" && i < len(s) && s[i] == 's' {
i++ // Consume the 's'.
unit = "ms"
}
// Convert to duration based on unit.
var d time.Duration
switch unit {
case "ns":
d = time.Nanosecond * time.Duration(num)
case "us", "µs":
d = time.Microsecond * time.Duration(num)
case "ms":
d = time.Millisecond * time.Duration(num)
case "s":
d = time.Second * time.Duration(num)
case "m":
d = time.Minute * time.Duration(num)
case "h":
d = time.Hour * time.Duration(num)
case "d":
d = 24 * time.Hour * time.Duration(num)
case "w":
d = 7 * 24 * time.Hour * time.Duration(num)
case "y":
d = 365 * 24 * time.Hour * time.Duration(num) // Approximate, non-leap year.
default:
return 0, fmt.Errorf("unknown unit %q at position %d", s[unitStart:i], unitStart)
}
total += d
}
return total, nil
}
type HexEncodeMode uint8
const (

View File

@@ -2,10 +2,46 @@ package lib_test
import (
"testing"
"time"
"git.wntrmute.dev/kyle/goutils/lib"
)
func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
wantErr bool
}{
// Valid durations
{"hour", "1h", time.Hour, false},
{"day", "2d", 2 * 24 * time.Hour, false},
{"minute", "3m", 3 * time.Minute, false},
{"second", "4s", 4 * time.Second, false},
// Edge cases
{"zero seconds", "0s", 0, false},
{"empty string", "", 0, true},
{"no numeric before unit", "h", 0, true},
{"invalid unit", "1x", 0, true},
{"non-numeric input", "abc", 0, true},
{"missing unit", "10", 0, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := lib.ParseDuration(tc.input)
if (err != nil) != tc.wantErr {
t.Fatalf("unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if got != tc.expected {
t.Fatalf("expected %v, got %v", tc.expected, got)
}
})
}
}
func TestHexEncode_LowerUpper(t *testing.T) {
b := []byte{0x0f, 0xa1, 0x00, 0xff}