From 051a2440835d2515024089b38051e6cbb9a44122 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 4 May 2026 14:28:53 -0600 Subject: [PATCH] fix(ipcohort): never return nil Cohort, zero value is empty set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All constructors (LoadFile, LoadFiles, ReadAll) now return &Cohort{} on error instead of nil. Callers can use the result directly without nil checks — an empty Cohort is a valid empty set that returns false for all Contains calls. --- net/ipcohort/ipcohort.go | 27 ++++++++++++--------------- net/ipcohort/middleware.go | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 net/ipcohort/middleware.go diff --git a/net/ipcohort/ipcohort.go b/net/ipcohort/ipcohort.go index 5a5ff32..bb33b1d 100644 --- a/net/ipcohort/ipcohort.go +++ b/net/ipcohort/ipcohort.go @@ -158,13 +158,13 @@ func ParseIPv4(raw string) (ipv4net IPv4Net, err error) { ), nil } -// LoadFile reads path and parses it into a Cohort. On open or read -// errors, returns nil + error. On parse errors only, returns the partial -// cohort + a joined error. +// LoadFile reads path and parses it into a Cohort. Always returns a +// non-nil Cohort (empty on error) so callers can use the zero value +// as an empty set without nil checks. func LoadFile(path string) (*Cohort, error) { f, err := os.Open(path) if err != nil { - return nil, fmt.Errorf("could not load %q: %w", path, err) + return &Cohort{}, fmt.Errorf("could not load %q: %w", path, err) } defer func() { _ = f.Close() }() return ParseCSV(f) @@ -173,21 +173,17 @@ func LoadFile(path string) (*Cohort, error) { // LoadFiles loads and merges multiple files into one Cohort. Useful when // hosts and networks are stored in separate files. // -// On open or read errors, returns nil + error. On parse errors only, -// returns the partial cohort + a joined error so callers can choose to -// proceed with what loaded. +// Always returns a non-nil Cohort. On errors, the cohort contains +// whatever loaded successfully; the caller decides whether to proceed. func LoadFiles(paths ...string) (*Cohort, error) { var hosts []uint32 var nets []IPv4Net - var parseErrs []error + var errs []error for _, path := range paths { c, err := LoadFile(path) - if c == nil { - return nil, err - } if err != nil { - parseErrs = append(parseErrs, fmt.Errorf("%s: %w", path, err)) + errs = append(errs, fmt.Errorf("%s: %w", path, err)) } hosts = append(hosts, c.hosts...) nets = append(nets, c.nets...) @@ -197,8 +193,8 @@ func LoadFiles(paths ...string) (*Cohort, error) { sortNets(nets) c := &Cohort{hosts: hosts, nets: nets} - if len(parseErrs) > 0 { - return c, errors.Join(parseErrs...) + if len(errs) > 0 { + return c, errors.Join(errs...) } return c, nil } @@ -225,7 +221,8 @@ func ReadAll(r *csv.Reader) (*Cohort, error) { break } if err != nil { - return nil, fmt.Errorf("csv read error: %w", err) + c := &Cohort{hosts: hosts, nets: nets} + return c, fmt.Errorf("csv read error: %w", err) } if len(record) == 0 { diff --git a/net/ipcohort/middleware.go b/net/ipcohort/middleware.go new file mode 100644 index 0000000..bc897da --- /dev/null +++ b/net/ipcohort/middleware.go @@ -0,0 +1,19 @@ +package ipcohort + +import ( + "net/http" + "strings" +) + +// HTTP middleware example +func (c *Cohort) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get client IP (basic; improve with X-Forwarded-For parsing if behind proxy) + ip, _, _ := strings.Cut(r.RemoteAddr, ":") + if blocked, _ := c.Contains(ip); blocked { + http.Error(w, "Access denied", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +}