Repository navigation
feat: implement M2 contracts (issues 5, 6, 7, 8) #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Scan Lifecycle Events | ||
|
|
||
| The `catnet-core` engine exposes a stream of typed events during scan execution to allow consumers (CLI, TUI, GUI) to react to progress and lifecycle changes without coupling to internal logic. | ||
|
|
||
| ## Event Signature | ||
|
|
||
| Events are dispatched synchronously via the `engine.EventCallback` function: | ||
| ```go | ||
| type EventCallback func(event ScanEvent) | ||
| ``` | ||
|
|
||
| ## ScanEvent Struct | ||
|
|
||
| ```go | ||
| type ScanEvent struct { | ||
| Type ScanEventType | ||
| Device *results.DeviceInfo | ||
| Progress float64 | ||
| Message string | ||
| } | ||
| ``` | ||
|
|
||
| ## Event Types | ||
|
|
||
| | Type | Description | Included Data | | ||
| |------|-------------|---------------| | ||
| | `EventLifecycleStart` | Fired once when `StartScan` is invoked. | `Message` | | ||
| | `EventLifecycleComplete` | Fired once when `StartScan` completes successfully. | `Message` | | ||
| | `EventLifecycleCancel` | Fired once when the scan is cancelled or times out. | `Message` | | ||
| | `EventWarning` | Fired when a non-fatal error occurs (e.g. permission issues). | `Message` | | ||
| | `EventProgress` | Fired periodically to indicate scan progress. | `Progress` (0.0 to 1.0) | | ||
| | `EventResult` | Fired when a device scan completes (whether alive or dead). | `Device` | | ||
|
|
||
| ## Consumer Responsibilities | ||
|
|
||
| 1. **Non-blocking callbacks:** Event callbacks are executed synchronously by the engine's worker goroutines. Consumers **must not** perform long-blocking operations in the callback. If processing requires heavy I/O, dispatch it to a channel or separate goroutine. | ||
| 2. **Thread safety:** The `Device` pointer in `EventResult` points to a copied value, but consumers should avoid mutating it. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package coreerr | ||
|
|
||
| import "errors" | ||
|
|
||
| // Sentinel errors representing the structured taxonomy for core operations. | ||
| var ( | ||
| ErrInvalidInput = errors.New("invalid input") | ||
| ErrTimeout = errors.New("operation timed out") | ||
| ErrCancelled = errors.New("operation cancelled") | ||
| ErrPermission = errors.New("permission denied") | ||
| ErrExport = errors.New("export failed") | ||
| ErrPartial = errors.New("partial failure") | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package coreerr | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestErrorWrapping(t *testing.T) { | ||
| err := fmt.Errorf("scan failed: %w", ErrTimeout) | ||
|
|
||
| if !errors.Is(err, ErrTimeout) { | ||
| t.Errorf("Expected err to wrap ErrTimeout, but errors.Is returned false") | ||
| } | ||
|
|
||
| if errors.Is(err, ErrCancelled) { | ||
| t.Errorf("Expected err to not wrap ErrCancelled") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,13 +5,15 @@ import ( | |||||||||||||||||||
| "fmt" | ||||||||||||||||||||
| "net" | ||||||||||||||||||||
| "strings" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| "github.com/mendsec/catnet-core/pkg/coreerr" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ParseRange interpreta uma string e retorna uma lista de endereços IP correspondentes. | ||||||||||||||||||||
| func ParseRange(input string) ([]string, error) { | ||||||||||||||||||||
| input = strings.TrimSpace(input) | ||||||||||||||||||||
| if input == "" { | ||||||||||||||||||||
| return nil, fmt.Errorf("empty input") | ||||||||||||||||||||
| return nil, fmt.Errorf("%w: empty input", coreerr.ErrInvalidInput) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if strings.Contains(input, "/") { | ||||||||||||||||||||
| return parseCIDR(input) | ||||||||||||||||||||
|
Comment on lines
18
to
19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error returned by Please wrap the error returned by
Suggested change
|
||||||||||||||||||||
|
|
@@ -21,7 +23,7 @@ func ParseRange(input string) ([]string, error) { | |||||||||||||||||||
| } | ||||||||||||||||||||
| parsed := net.ParseIP(input) | ||||||||||||||||||||
| if parsed == nil { | ||||||||||||||||||||
| return nil, fmt.Errorf("invalid IP format") | ||||||||||||||||||||
| return nil, fmt.Errorf("%w: invalid IP format", coreerr.ErrInvalidInput) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return []string{parsed.String()}, nil | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -44,7 +46,7 @@ func parseCIDR(cidr string) ([]string, error) { | |||||||||||||||||||
| func parseDashRange(dashStr string) ([]string, error) { | ||||||||||||||||||||
| parts := strings.Split(dashStr, "-") | ||||||||||||||||||||
| if len(parts) != 2 { | ||||||||||||||||||||
| return nil, fmt.Errorf("invalid range format, expected start-end") | ||||||||||||||||||||
| return nil, fmt.Errorf("%w: invalid range format, expected start-end", coreerr.ErrInvalidInput) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| startStr := strings.TrimSpace(parts[0]) | ||||||||||||||||||||
| endStr := strings.TrimSpace(parts[1]) | ||||||||||||||||||||
|
|
@@ -57,15 +59,15 @@ func parseDashRange(dashStr string) ([]string, error) { | |||||||||||||||||||
| startIP := net.ParseIP(startStr).To4() | ||||||||||||||||||||
| endIP := net.ParseIP(endStr).To4() | ||||||||||||||||||||
| if startIP == nil || endIP == nil { | ||||||||||||||||||||
| return nil, fmt.Errorf("invalid IP in range") | ||||||||||||||||||||
| return nil, fmt.Errorf("%w: invalid IP in range", coreerr.ErrInvalidInput) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| start := binary.BigEndian.Uint32(startIP) | ||||||||||||||||||||
| end := binary.BigEndian.Uint32(endIP) | ||||||||||||||||||||
| if start > end { | ||||||||||||||||||||
| return nil, fmt.Errorf("start IP is greater than end IP") | ||||||||||||||||||||
| return nil, fmt.Errorf("%w: start IP is greater than end IP", coreerr.ErrInvalidInput) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if end-start > 65536 { | ||||||||||||||||||||
| return nil, fmt.Errorf("range too large (max 65536)") | ||||||||||||||||||||
| return nil, fmt.Errorf("%w: range too large (max 65536)", coreerr.ErrInvalidInput) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| var ips []string | ||||||||||||||||||||
| for i := start; i <= end; i++ { | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
onEventcallback is invoked concurrently from multiple worker goroutines. It is crucial to explicitly document this behavior so that consumers know they must implement thread-safe callbacks when mutating shared state or updating UI components.