From 11f2ce5162ccbfc0ce0f4c96dab743c8a10bda13 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sat, 29 Jun 2024 20:38:31 +0300 Subject: [PATCH 01/12] design refactor --- backlog.todo | 7 + cmd/supernet/main.go | 21 +- cmd/supernet/parser.go | 2 +- desgin.md | 9 + pkg/supernet/action.go | 156 ++++ pkg/supernet/conflict.go | 109 +++ pkg/supernet/plan.go | 19 + pkg/supernet/supernet.go | 361 ++++++++++ .../supernet/supernet_test.go | 214 +++--- pkg/supernet/uitls.go | 138 ++++ pkg/trie/trie.go | 109 +-- pkg/trie/trie_test.go | 70 +- supernet.go | 680 ------------------ 13 files changed, 1018 insertions(+), 877 deletions(-) create mode 100644 desgin.md create mode 100644 pkg/supernet/action.go create mode 100644 pkg/supernet/conflict.go create mode 100644 pkg/supernet/plan.go create mode 100644 pkg/supernet/supernet.go rename supernet_test.go => pkg/supernet/supernet_test.go (68%) create mode 100644 pkg/supernet/uitls.go delete mode 100644 supernet.go diff --git a/backlog.todo b/backlog.todo index 55a2ecf..8fcea61 100644 --- a/backlog.todo +++ b/backlog.todo @@ -8,3 +8,10 @@ ✔ @Feat(Supernet): add recursive conflict resolution during splitting @high @high @done(24-06-25 20:32) ✔ replace AddChildIfNotExist with super.Insert and refactor the code if needed @done(24-06-25 20:32) ✔ find a way if to detect if the conflicted was created during the insertion, to avoid conflict resolution @done(24-06-25 20:32) + ☐ @Refactor(Supernet): clean the code + ☐ @Docs(Supernet): rewrite the comments + ☐ @Refactor(Supernet): make the resolvers and actions easy to track + ☐ @feat(Supernet): make the supernet configurable + ☐ @feat(Supernet): add custom comparator + + diff --git a/cmd/supernet/main.go b/cmd/supernet/main.go index 890c59b..c71aa77 100644 --- a/cmd/supernet/main.go +++ b/cmd/supernet/main.go @@ -8,7 +8,7 @@ import ( "os" "github.com/alecthomas/kong" - "github.com/khalid_nowaf/supernet" + "github.com/khalid_nowaf/supernet/pkg/supernet" ) // ResolveCmd represents the command to resolve CIDR conflicts. @@ -40,11 +40,8 @@ func (cmd *ResolveCmd) Run(ctx *kong.Context) error { // parseAndInsertCidrs parses a file and inserts CIDRs into the supernet. func parseAndInsertCidrs(super *supernet.Supernet, cmd *ResolveCmd, file string) error { return parseCsv(cmd, file, func(cidr *CIDR) error { - fmt.Printf("\nInserting CIDR: %s with P %v ...\n", cidr.cidr.String(), cidr.Priority) - results := super.InsertCidr(cidr.cidr, cidr.Metadata) - for _, r := range results { - fmt.Printf(r.String()) - } + result := super.InsertCidr(cidr.cidr, cidr.Metadata) + fmt.Println(result.String()) return nil }) } @@ -67,13 +64,13 @@ func writeJsonResults(super *supernet.Supernet, directory string, cidrCol string } for i, cidr := range cidrs { - cidr.Metadata.Attributes[cidrCol] = supernet.NodeToCidr(cidr) + cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) if i > 0 { if _, err = file.Write([]byte(",")); err != nil { return err } } - if err = encoder.Encode(cidr.Metadata.Attributes); err != nil { + if err = encoder.Encode(cidr.Metadata().Attributes); err != nil { return err } } @@ -104,7 +101,7 @@ func writeCsvResults(super *supernet.Supernet, directory string, cidrCol string) // Optional: Write headers to the CSV file headers := []string{} - for key := range cidrs[0].Metadata.Attributes { + for key := range cidrs[0].Metadata().Attributes { headers = append(headers, key) } if err := writer.Write(headers); err != nil { @@ -113,11 +110,11 @@ func writeCsvResults(super *supernet.Supernet, directory string, cidrCol string) // Write data to the CSV file for _, cidr := range cidrs { - cidr.Metadata.Attributes[cidrCol] = supernet.NodeToCidr(cidr) - record := make([]string, 0, len(cidr.Metadata.Attributes)) + cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) + record := make([]string, 0, len(cidr.Metadata().Attributes)) // Ensure the fields are written in the same order as headers for _, header := range headers { - record = append(record, cidr.Metadata.Attributes[header]) + record = append(record, cidr.Metadata().Attributes[header]) } if err := writer.Write(record); err != nil { return err diff --git a/cmd/supernet/parser.go b/cmd/supernet/parser.go index d982828..00289c0 100644 --- a/cmd/supernet/parser.go +++ b/cmd/supernet/parser.go @@ -9,7 +9,7 @@ import ( "strconv" "strings" - "github.com/khalid_nowaf/supernet" + "github.com/khalid_nowaf/supernet/pkg/supernet" ) type CIDR struct { diff --git a/desgin.md b/desgin.md new file mode 100644 index 0000000..08affb3 --- /dev/null +++ b/desgin.md @@ -0,0 +1,9 @@ +## Conflict Reporter +- it should says the new cidr is conflicting with what? (Conflicted With) +- it should says what type of the conflict is happing (Conflict Type) +- it should says what is the actions is taken (ResolutionAction) +- it should says what happened (Added and Removed CIDRS) + + +### Edge case: If the inserted CIDR is a supernet of N subnet: +- each one conflict should have a conflict Report diff --git a/pkg/supernet/action.go b/pkg/supernet/action.go new file mode 100644 index 0000000..7ef927b --- /dev/null +++ b/pkg/supernet/action.go @@ -0,0 +1,156 @@ +package supernet + +import ( + "fmt" + + "github.com/khalid_nowaf/supernet/pkg/trie" +) + +type Action interface { + Execute(newCidr *trie.BinaryTrie[Metadata], conflictedPoint *trie.BinaryTrie[Metadata], targetNode *trie.BinaryTrie[Metadata], remainingPath []int) *ActionResult + String() string +} + +type ( + IgnoreInsertion struct{} // no op Action + InsertNewCIDR struct{} // Inserted the new CIDR `on` specific node + RemoveExistingCIDR struct{} // remove existing CIDR `on` specific node + SplitInsertedCIDR struct{} // split the new CIDR `on` specific node + SplitExistingCIDR struct{} // split the existing CIDR `on` specific node +) + +func (action IgnoreInsertion) Execute(_ *trie.BinaryTrie[Metadata], _ *trie.BinaryTrie[Metadata], _ *trie.BinaryTrie[Metadata], _ []int) *ActionResult { + return &ActionResult{ + Action: action, + } +} + +func (_ IgnoreInsertion) String() string { + return "Ignore Insertion" +} + +func (action InsertNewCIDR) Execute(newCidr *trie.BinaryTrie[Metadata], conflictedPoint *trie.BinaryTrie[Metadata], _ *trie.BinaryTrie[Metadata], remainingPath []int) *ActionResult { + + actionResult := &ActionResult{ + Action: action, + } + + // sanity checks + if conflictedPoint == nil { + panic("[BUG] Action[InsertNewCIDR].Execute:: conflictedPoint Node must not be nil") + } + if !conflictedPoint.IsLeaf() { + panic("[BUG] Action[InsertNewCIDR].Execute:: conflictedPoint node must be a leaf") + } + + lastNode, conflictType, _ := buildPath(conflictedPoint, remainingPath) + if _, noConflict := conflictType.(NoConflict); !noConflict { + panic("[BUG] Action[InsertNewCIDR].Execute:: Can not insert CIDR while there is a conflict unresolved") + } + + // what if last node has metadata! + if lastNode.Metadata() != nil { + panic("[BUG] Action[InsertNewCIDR].Execute:: Last node must be path node without metadata") + } + lastNode.Parent().ReplaceChild(newCidr, lastNode.Pos()) + actionResult.appendAddedCidr(newCidr) + return actionResult + +} + +func (_ InsertNewCIDR) String() string { + return "Insert New CIDR" +} + +func (action RemoveExistingCIDR) Execute(newCidr *trie.BinaryTrie[Metadata], _ *trie.BinaryTrie[Metadata], targetNode *trie.BinaryTrie[Metadata], _ []int) *ActionResult { + + actionResult := &ActionResult{ + Action: action, + } + + actionResult.appendRemovedCidr(targetNode) + newCidrDepth, _ := newCidr.Metadata().originCIDR.Mask.Size() + + if newCidrDepth >= targetNode.Depth() { + targetNode.UpdateMetadata(nil) + } else if newCidrDepth < targetNode.Depth() { + targetNode.DetachBranch(newCidrDepth + 1) + } + + return actionResult + +} + +func (_ RemoveExistingCIDR) String() string { + return "Remove Existing CIDR" +} + +func (action SplitInsertedCIDR) Execute(newCidr *trie.BinaryTrie[Metadata], conflictedPoint *trie.BinaryTrie[Metadata], targetNode *trie.BinaryTrie[Metadata], _ []int) *ActionResult { + + actionResult := &ActionResult{ + Action: action, + } + + splittedCidr := splitAround(targetNode, newCidr.Metadata(), conflictedPoint.Depth()) + + for _, addedCidr := range splittedCidr { + actionResult.appendAddedCidr(addedCidr) + } + + return actionResult + +} + +func (_ SplitInsertedCIDR) String() string { + return "Split Inserted CIDR" +} + +func (action SplitExistingCIDR) Execute(newCidr *trie.BinaryTrie[Metadata], conflictedPoint *trie.BinaryTrie[Metadata], targetNode *trie.BinaryTrie[Metadata], _ []int) *ActionResult { + // init inserted result + actionResult := &ActionResult{ + Action: action, + } + + splittedCidrs := splitAround(newCidr, targetNode.Metadata(), conflictedPoint.Depth()) + + for _, splittedCidr := range splittedCidrs { + actionResult.appendAddedCidr(splittedCidr) + } + return actionResult + +} + +func (_ SplitExistingCIDR) String() string { + return "Split Existing CIDR" +} + +type ActionResult struct { + Action Action + AddedCidrs []trie.BinaryTrie[Metadata] + RemoveCidrs []trie.BinaryTrie[Metadata] +} + +func (ar ActionResult) String() string { + addedCidrs := []string{} + removedCidrs := []string{} + + for _, added := range ar.AddedCidrs { + addedCidrs = append(addedCidrs, NodeToCidr(&added)) + } + + for _, removed := range ar.RemoveCidrs { + removedCidrs = append(removedCidrs, NodeToCidr(&removed)) + } + + return fmt.Sprintf("Action Taken: %s, Added CIDRs: %v, Removed CIDRs: %v", ar.Action, addedCidrs, removedCidrs) +} + +// to keep track of all the added CIDRs from resolving a conflict. +func (ar *ActionResult) appendAddedCidr(cidr *trie.BinaryTrie[Metadata]) { + ar.AddedCidrs = append(ar.AddedCidrs, *cidr) +} + +// to keep track of all removed CIDRs from resolving a conflict. +func (ar *ActionResult) appendRemovedCidr(cidr *trie.BinaryTrie[Metadata]) { + ar.RemoveCidrs = append(ar.RemoveCidrs, *cidr) +} diff --git a/pkg/supernet/conflict.go b/pkg/supernet/conflict.go new file mode 100644 index 0000000..ef569d4 --- /dev/null +++ b/pkg/supernet/conflict.go @@ -0,0 +1,109 @@ +package supernet + +import "github.com/khalid_nowaf/supernet/pkg/trie" + +type ConflictType interface { + String() string + Resolve(conflictedCidr *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan +} + +type ( + NoConflict struct{} // there is no conflict + EqualCIDR struct{} // the new CIDR equal an existing CIDR + SuperCIDR struct{} // The new CIDR is a super CIDR of one or more existing sub CIDRs + SubCIDR struct{} // the new CIDR is a sub CIDR of an existing super CIDR +) + +func (_ NoConflict) Resolve(at *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { + plan := &ResolutionPlan{} + plan.AddAction(InsertNewCIDR{}, at) + return plan +} + +func (_ NoConflict) String() string { + return "No Conflict" +} + +func (_ EqualCIDR) Resolve(conflictedCidr *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { + plan := &ResolutionPlan{} + plan.Conflicts = append(plan.Conflicts, *conflictedCidr) + if comparator(newCidr.Metadata(), conflictedCidr.Metadata()) { + + plan.AddAction(RemoveExistingCIDR{}, conflictedCidr) + plan.AddAction(InsertNewCIDR{}, conflictedCidr) + } else { + plan.AddAction(IgnoreInsertion{}, newCidr) + + } + return plan +} + +func (_ EqualCIDR) String() string { + return "Equal CIDR" +} + +func (_ SuperCIDR) Resolve(conflictPoint *trie.BinaryTrie[Metadata], newSuperCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { + plan := &ResolutionPlan{} + + // since this is a super, we do not know how many subcidrs yet conflicting with this super + // let us get all subCidrs + conflictedSubCidrs := conflictPoint.GetLeafs() + + subCidrsWithLowPriority := []*trie.BinaryTrie[Metadata]{} + subCidrsWithHighPriority := []*trie.BinaryTrie[Metadata]{} + + for _, conflictedSubCidr := range conflictedSubCidrs { + plan.Conflicts = append(plan.Conflicts, *conflictedSubCidr) + if comparator(newSuperCidr.Metadata(), conflictedSubCidr.Metadata()) { + subCidrsWithLowPriority = append(subCidrsWithLowPriority, conflictedSubCidr) + // new cidr has higher priority + } else { + subCidrsWithHighPriority = append(subCidrsWithHighPriority, conflictedSubCidr) + } + } + + // now we deal with conflicted cidrs that needed to be removed + for _, toBeRemoved := range subCidrsWithLowPriority { + plan.AddAction(RemoveExistingCIDR{}, toBeRemoved) + } + + // then we split the removed cidrs + for _, toBeSplittedAround := range subCidrsWithHighPriority { + plan.AddAction(SplitInsertedCIDR{}, toBeSplittedAround) + } + + // lastly, we can insert the new cidr without conflict + if len(subCidrsWithHighPriority) == 0 { + plan.AddAction(InsertNewCIDR{}, conflictPoint) + } + + return plan +} + +func (_ SuperCIDR) String() string { + return "Super CIDR" +} + +func (_ SubCIDR) Resolve(existingSuperCidr *trie.BinaryTrie[Metadata], newSubCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { + plan := &ResolutionPlan{} + plan.Conflicts = append(plan.Conflicts, *existingSuperCidr) + // since this is a SubCidr, we have 2 option + // - ignore it, if the SubCidr has low priority + // - split the super around this subCidr if Subcidr has low priority + + if comparator(newSubCidr.Metadata(), existingSuperCidr.Metadata()) { + // subcidr has higher priority + + plan.AddAction(InsertNewCIDR{}, newSubCidr) + plan.AddAction(SplitExistingCIDR{}, existingSuperCidr) + plan.AddAction(RemoveExistingCIDR{}, existingSuperCidr) + } else { + // subcidr has low priority + plan.AddAction(IgnoreInsertion{}, newSubCidr) + } + return plan +} + +func (_ SubCIDR) String() string { + return "Sub CIDR" +} diff --git a/pkg/supernet/plan.go b/pkg/supernet/plan.go new file mode 100644 index 0000000..ab76d17 --- /dev/null +++ b/pkg/supernet/plan.go @@ -0,0 +1,19 @@ +package supernet + +import "github.com/khalid_nowaf/supernet/pkg/trie" + +type PlanStep struct { + Action Action + TargetNode *trie.BinaryTrie[Metadata] +} +type ResolutionPlan struct { + Conflicts []trie.BinaryTrie[Metadata] + Steps []*PlanStep +} + +func (plan *ResolutionPlan) AddAction(action Action, on *trie.BinaryTrie[Metadata]) { + plan.Steps = append(plan.Steps, &PlanStep{ + Action: action, + TargetNode: on, + }) +} diff --git a/pkg/supernet/supernet.go b/pkg/supernet/supernet.go new file mode 100644 index 0000000..ca463c0 --- /dev/null +++ b/pkg/supernet/supernet.go @@ -0,0 +1,361 @@ +package supernet + +import ( + "fmt" + "net" + "strings" + + "github.com/khalid_nowaf/supernet/pkg/trie" +) + +func buildPath(root *trie.BinaryTrie[Metadata], path []int) (lastNode *trie.BinaryTrie[Metadata], conflict ConflictType, remainingPath []int) { + currentNode := root + for currentDepth, bit := range path { + // add a pathNode, if the current node is nil + currentNode = currentNode.AttachChild(newPathNode(), bit) + + conflictType := isThereAConflict(currentNode, len(path)) + + // if the there is a conflict, return the conflicting point node, and the remaining bits (path) + if _, noConflict := conflictType.(NoConflict); !noConflict { + return currentNode, conflictType, path[currentDepth+1:] + } + } + return currentNode, NoConflict{}, []int{} // empty +} + +func insertLeaf(root *trie.BinaryTrie[Metadata], path []int, newCidrNode *trie.BinaryTrie[Metadata]) *InsertionResult { + insertionResults := &InsertionResult{ + CIDR: newCidrNode.Metadata().originCIDR, + } + + // buildPath will tell us the strategy to resolve the conflict if there is + // any. + lastNode, conflictType, remainingPath := buildPath(root, path) + insertionResults.ConflictType = conflictType + + // based on the conflict we will get resolve + // and the resolver will return a resolution plan for each conflict + plan := conflictType.Resolve(lastNode, newCidrNode) + insertionResults.ConflictedWith = append(insertionResults.ConflictedWith, plan.Conflicts...) + + for _, step := range plan.Steps { + // each plan has an action has an excitor, and return an action result + result := step.Action.Execute(newCidrNode, lastNode, step.TargetNode, remainingPath) + insertionResults.actions = append(insertionResults.actions, result) + } + + return insertionResults +} + +// records the outcome of attempting to insert a CIDR for reporting +type InsertionResult struct { + CIDR *net.IPNet // CIDR was attempted to be inserted. + actions []*ActionResult // the result of each action is taken + ConflictedWith []trie.BinaryTrie[Metadata] // array of conflicting nodes + ConflictType // the type of the conflict +} + +func (ir *InsertionResult) String() string { + str := "" + + if _, ok := ir.ConflictType.(NoConflict); !ok { + str += fmt.Sprintf("Detect %s conflict |", ir.ConflictType) + str += fmt.Sprintf("New CIDR %s conflicted with [", ir.CIDR) + for _, conflictedCidr := range ir.ConflictedWith { + str += fmt.Sprintf("%s ", NodeToCidr(&conflictedCidr)) + } + str += "] | " + } + + for _, action := range ir.actions { + str += fmt.Sprintf("%s", action.String()) + } + + return str +} + +// holds the properties for a CIDR node within a trie, including IP version, priority, and additional attributes. +type Metadata struct { + originCIDR *net.IPNet // copy of the CIDR, to track it, if it get splitted later due to conflict resolution + IsV6 bool // is it IPv6 CIDR + Priority []uint8 // min value 0, max value 255, and all CIDR in the tree must have the same length + Attributes map[string]string // generic key value attributes to hold additional information about the CIDR +} + +// construct a Metadata for a cidr +func NewMetadata(ipnet *net.IPNet) *Metadata { + isV6 := false + if ipnet.IP.To4() == nil { + isV6 = true + } + return &Metadata{ + originCIDR: ipnet, + IsV6: isV6, + } +} + +// creates a Metadata instance with default values. +// +// Returns: +// - A pointer to a Metadata instance initialized with default values. +// func NewDefaultMetadata() *Metadata { +// return &Metadata{} +// } + +// Supernet represents a structure containing both IPv4 and IPv6 CIDRs, each stored in a separate trie. +type Supernet struct { + ipv4Cidrs *trie.BinaryTrie[Metadata] + ipv6Cidrs *trie.BinaryTrie[Metadata] +} + +// initializes a new supernet instance with separate tries for IPv4 and IPv6 CIDRs. +// +// Returns: +// - A pointer to a newly initialized supernet instance. +func NewSupernet() *Supernet { + return &Supernet{ + ipv4Cidrs: &trie.BinaryTrie[Metadata]{}, + ipv6Cidrs: &trie.BinaryTrie[Metadata]{}, + } +} + +// newPathNode creates a new trie node intended for path utilization without any associated metadata. +// +// Returns: +// - A pointer to a newly created trie.BinaryTrie node with no metadata. +func newPathNode() *trie.BinaryTrie[Metadata] { + return &trie.BinaryTrie[Metadata]{} +} + +// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. +// +// Parameters: +// - forV6: A boolean flag if th CIDR is IPv6 +// +// Returns: +// - A slice of TrieNode, each representing a CIDR in the specified trie. +func (super *Supernet) GetAllV4Cidrs(forV6 bool) []*trie.BinaryTrie[Metadata] { + supernet := super.ipv4Cidrs + if forV6 { + supernet = super.ipv6Cidrs + } + return supernet.GetLeafs() +} + +// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. +// +// Parameters: +// - forV6: A boolean flag if th CIDR is IPv6 +// +// Returns: +// - A slice of strings, each representing a CIDR in the specified trie. +func (super *Supernet) getAllV4CidrsString(forV6 bool) []string { + supernet := super.ipv4Cidrs + if forV6 { + supernet = super.ipv6Cidrs + } + var cidrs []string + for _, node := range supernet.GetLeafs() { + cidrs = append(cidrs, BitsToCidr(node.GetPath(), forV6).String()) + } + return cidrs +} + +// InsertCidr attempts to insert a new CIDR into the supernet, handling conflicts according to predefined priorities. +// It traverses through the trie, adding new nodes as needed and resolving conflicts when they occur. +// +// Parameters: +// - ipnet: net.IPNet, the CIDR to be inserted. +// - metadata: Metadata associated with the CIDR, used for conflict resolution and node creation. +// +// This function navigates through each bit of the new CIDR's path, trying to add a new node if it doesn't already exist, +// and handles various types of conflicts (EQUAL_CIDR, SUBCIDR, SUPERCIDR) by comparing the priorities of the involved CIDRs. + +func (super *Supernet) InsertCidr(ipnet *net.IPNet, metadata *Metadata) *InsertionResult { + + root := super.ipv4Cidrs + path, depth := CidrToBits(ipnet) + copyMetadata := metadata + if copyMetadata == nil { + copyMetadata = NewMetadata(ipnet) + } + + if ipnet.IP.To4() == nil { + copyMetadata.IsV6 = true + root = super.ipv6Cidrs + } + + if copyMetadata.IsV6 { + root = super.ipv6Cidrs + } + + // add size of the subnet as priory + copyMetadata.Priority = append(copyMetadata.Priority, uint8(depth)) + copyMetadata.originCIDR = ipnet + results := insertLeaf( + root, + path, + trie.NewTrieWithMetadata(copyMetadata), + ) + + return results +} + +// Conflict Types: +// - SUPERCIDR: The current node is a supernet relative to the targeted CIDR. +// - SUBCIDR: The current node is a subnetwork relative to the targeted CIDR. +// - EQUAL_CIDR: The current node and the targeted CIDR are at the same depth. +// - NONE: There is no conflict. +func isThereAConflict(currentNode *trie.BinaryTrie[Metadata], targetedDepth int) ConflictType { + // Check if the current node is a new or path node without specific metadata. + if currentNode.Metadata() == nil { + // Determine if the current node is a supernet of the targeted CIDR. + if targetedDepth == currentNode.Depth() && !currentNode.IsLeaf() { + return SuperCIDR{} // The node spans over the area of the new CIDR. + } else { + return NoConflict{} // No conflict detected. + } + } else { + // Evaluate the relationship based on depths. + if currentNode.Depth() == targetedDepth { + return EqualCIDR{} // The node is at the same level as the targeted CIDR. + } + if currentNode.Depth() < targetedDepth { + return SubCIDR{} // The node is a subnetwork of the targeted CIDR. + } + } + + // If none of the conditions are met, there's an unhandled case. + panic("[BUG] isThereAConflict: unhandled edge case encountered") +} + +// splitAround adjusts a super CIDR's trie structure around a specified sub CIDR by inserting sibling nodes. +// This process involves branching off at each step from the SUB-CIDR node upwards towards the SUPER-CIDR node, +// ensuring that the appropriate splits in the trie are made to represent the network structure correctly. +// +// Parameters: +// - super: super CIDR. +// - sub: sub CIDR to be surrounded. +// - splittedCidrMetadata: Metadata for the new CIDR nodes that will be created during the split. +// +// Returns: +// - A slice of pointers to nodes that were newly added during the splitting process. +// +// Panics: +// - If splittedCidrMetadata is nil, as metadata is essential for creating new trie nodes. +// +// The function traverses from the sub-CIDR node upwards, attempting to insert a sibling node at each step. +// If a sibling node at a given position does not exist, it is created and added. The traversal and modifications +// stop when reaching the depth of the super-CIDR node. +func splitAround(sub *trie.BinaryTrie[Metadata], newCidrMetadata *Metadata, limitDepth int) []*trie.BinaryTrie[Metadata] { + splittedCidrMetadata := newCidrMetadata + + if splittedCidrMetadata == nil { + panic("[BUG] splitAround: Metadata is required to split a supernet") + } + + var splittedCidrs []*trie.BinaryTrie[Metadata] + + sub.ForEachStepUp(func(current *trie.BinaryTrie[Metadata]) { + + // Create a new trie node with the same metadata as the splittedCidrMetadata. + newCidr := trie.NewTrieWithMetadata(&Metadata{ + IsV6: splittedCidrMetadata.IsV6, + originCIDR: splittedCidrMetadata.originCIDR, + Priority: splittedCidrMetadata.Priority, + Attributes: splittedCidrMetadata.Attributes, // Additional attributes from metadata. + }) + + added := current.AttachSibling(newCidr) + + if added == newCidr { + // If the node was successfully added, append it to the list of split CIDRs. + splittedCidrs = append(splittedCidrs, added) + } else { + } + }, func(nextNode *trie.BinaryTrie[Metadata]) bool { + // Stop propagation when reaching the depth of the super-CIDR. + return nextNode.Depth() > limitDepth + }) + + return splittedCidrs +} + +// LookupIP searches for the closest matching CIDR for a given IP address within the supernet. +// +// Parameters: +// - ip: A string representing the IP address +// +// Returns: +// - net.IPNet representing the closest matching CIDR, if found, or nil +// - An error if the IP address cannot be parsed +// +// The function parses the input IP address into a CIDR with a full netmask (32 for IPv4, 128 for IPv6). +// It then converts this CIDR into a slice of bits and traverses the corresponding trie (IPv4 or IPv6) +// to find the most specific matching CIDR. If the trie node representing the CIDR is a leaf or no further +// children exist for matching, the search concludes, returning the found CIDR or nil if no match exists. +func (super *Supernet) LookupIP(ip string) (*net.IPNet, error) { + // Determine if the IP is IPv4 or IPv6 based on the presence of a colon. + isV6 := strings.Contains(ip, ":") + mask := 32 + supernet := super.ipv4Cidrs // Default to IPv4 supernet. + + if isV6 { + mask = 128 + supernet = super.ipv6Cidrs // Use IPv6 supernet if the IP is IPv6. + } + + // Parse the IP address with a full netmask to form a valid CIDR for bit conversion. + _, parsedIP, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ip, mask)) + if err != nil { + return nil, err // Return parsing errors. + } + + ipBits, _ := CidrToBits(parsedIP) // Convert the parsed CIDR to a slice of bits. + + // Traverse the trie to find the most specific matching CIDR. + for i, bit := range ipBits { + if supernet == nil { + // Return nil if no matching CIDR is found in the trie. + return nil, nil + } else if supernet.IsLeaf() { + // Return the CIDR up to the current bit index if a leaf node is reached. + return BitsToCidr(ipBits[:i], isV6), nil + } else { + // Move to the next child node based on the current bit. + supernet = supernet.Child(bit) + } + } + + // The loop should always return before reaching this point. + panic("[BUG] LookupIP: reached an unexpected state, the CIDR trie traversal should not get here.") +} + +// comparator evaluates two trie nodes, `a` and `b`, to determine if the new node `a` should replace the old node `b` +// based on their priority values. It is assumed that `a` is the new node and `b` is the old node. +// +// Parameters: +// - a: new CIDR entrya. +// - b: the old CIDR entry. +// +// Returns: +// - true if `a` should replace `b` or if they are considered equal in priority; false otherwise. +// +// Note: +// - The function assumes that if all priorities of `a` are equal to `b`, then `a` should be greater than `b`. +// - The priorities are compared in a lexicographical order, similar to comparing version numbers or tuples. +func comparator(a *Metadata, b *Metadata) bool { + // Compare priority values lexicographically. + for i := range a.Priority { + if a.Priority[i] > b.Priority[i] { + + // If any priority of 'a' is less than 'b', return false immediately. + return true + } else if a.Priority[i] < b.Priority[i] { + return false + } + } + // they are equal, so a is greater + return true +} diff --git a/supernet_test.go b/pkg/supernet/supernet_test.go similarity index 68% rename from supernet_test.go rename to pkg/supernet/supernet_test.go index 0e2af11..1a94b3f 100644 --- a/supernet_test.go +++ b/pkg/supernet/supernet_test.go @@ -12,13 +12,13 @@ func TestPanicsWithZeroCIDRMask(t *testing.T) { // Test with IPv4 zero mask _, cidrIPv4, _ := net.ParseCIDR("1.1.1.1/0") assert.Panics(t, func() { - cidrToBits(cidrIPv4) + CidrToBits(cidrIPv4) }, "Should panic with IPv4 zero CIDR mask") // Test with IPv6 zero mask _, cidrIPv6, _ := net.ParseCIDR("2001:db8::ff00:42:8329/0") assert.Panics(t, func() { - cidrToBits(cidrIPv6) + CidrToBits(cidrIPv6) }, "Should panic with IPv6 zero CIDR mask") } @@ -35,7 +35,7 @@ func TestCIDRToBitsConversion(t *testing.T) { for _, tc := range testCases { _, cidr, err := net.ParseCIDR(tc.cidr) - bits, depth := cidrToBits(cidr) + bits, depth := CidrToBits(cidr) assert.NoError(t, err) assert.Equal(t, tc.expectedDepth, depth) assert.Equal(t, tc.expectedBits, bits) @@ -55,14 +55,14 @@ func TestBitsToCIDRConversion(t *testing.T) { for _, tc := range testCases { _, cidr, _ := net.ParseCIDR(tc.cidr) - bits, _ := cidrToBits(cidr) - assert.Equal(t, cidr.String(), bitsToCidr(bits, tc.isIPv6).String()) + bits, _ := CidrToBits(cidr) + assert.Equal(t, cidr.String(), BitsToCidr(bits, tc.isIPv6).String()) } } func TestTrieComparator(t *testing.T) { - a := newPathTrie() - b := newPathTrie() + a := newPathNode() + b := newPathNode() // Comparator scenarios comparisons := []struct { @@ -74,12 +74,13 @@ func TestTrieComparator(t *testing.T) { {[]uint8{0, 1, 1}, []uint8{1, 0, 0}, false}, {[]uint8{1, 1, 1}, []uint8{1, 1, 1}, true}, {[]uint8{0, 0, 1}, []uint8{0, 1, 0}, false}, + {[]uint8{1, 0, 16}, []uint8{0, 0, 32}, true}, } for _, comp := range comparisons { - a.Metadata = &Metadata{Priority: comp.aPriority} - b.Metadata = &Metadata{Priority: comp.bPriority} - assert.Equal(t, comp.expected, comparator(a, b)) + a.UpdateMetadata(&Metadata{Priority: comp.aPriority}) + b.UpdateMetadata(&Metadata{Priority: comp.bPriority}) + assert.Equal(t, comp.expected, comparator(a.Metadata(), b.Metadata())) } } @@ -89,7 +90,9 @@ func TestInsertAndRetrieveCidrs(t *testing.T) { for _, cidrString := range cidrs { _, cidr, _ := net.ParseCIDR(cidrString) - super.InsertCidr(cidr, nil) + results := super.InsertCidr(cidr, nil) + printPaths(super) + printResults(results) } ipv4Results := []string{"1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8"} @@ -105,15 +108,16 @@ func TestEqualConflictLowPriory(t *testing.T) { _, cidrHigh, _ := net.ParseCIDR("192.168.0.0/16") _, cidrLow, _ := net.ParseCIDR("192.168.0.0/16") - root.InsertCidr(cidrHigh, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr("high")}) - results := root.InsertCidr(cidrLow, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr("low")}) + root.InsertCidr(cidrHigh, &Metadata{Priority: []uint8{1}, originCIDR: cidrHigh, Attributes: makeCidrAtrr("high")}) + results := root.InsertCidr(cidrLow, &Metadata{Priority: []uint8{0}, originCIDR: cidrLow, Attributes: makeCidrAtrr("low")}) + printPaths(root) printResults(results) // subset assert.ElementsMatch(t, []string{ "192.168.0.0/16", }, root.getAllV4CidrsString(false)) - assert.Equal(t, "high", root.ipv4Cidrs.GetLeafs()[0].Metadata.Attributes["cidr"]) + assert.Equal(t, "high", root.ipv4Cidrs.GetLeafs()[0].Metadata().Attributes["cidr"]) } func TestEqualConflictHighPriory(t *testing.T) { @@ -122,15 +126,15 @@ func TestEqualConflictHighPriory(t *testing.T) { _, cidrHigh, _ := net.ParseCIDR("192.168.0.0/16") _, cidrLow, _ := net.ParseCIDR("192.168.0.0/16") - root.InsertCidr(cidrLow, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr("low")}) - result := root.InsertCidr(cidrHigh, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr("high")}) + root.InsertCidr(cidrLow, &Metadata{Priority: []uint8{0}, originCIDR: cidrLow, Attributes: makeCidrAtrr("low")}) + result := root.InsertCidr(cidrHigh, &Metadata{Priority: []uint8{1}, originCIDR: cidrHigh, Attributes: makeCidrAtrr("high")}) printResults(result) // subset assert.ElementsMatch(t, []string{ "192.168.0.0/16", }, root.getAllV4CidrsString(false)) - assert.Equal(t, "high", root.ipv4Cidrs.GetLeafs()[0].Metadata.Attributes["cidr"]) + assert.Equal(t, "high", root.ipv4Cidrs.GetLeafs()[0].Metadata().Attributes["cidr"]) } @@ -153,9 +157,10 @@ func TestSubConflictHighPriority(t *testing.T) { _, super, _ := net.ParseCIDR("192.168.0.0/16") _, sub, _ := net.ParseCIDR("192.168.1.1/24") - root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) - root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(sub.String())}) - + root.InsertCidr(super, &Metadata{Priority: []uint8{0}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) + results := root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) + printPaths(root) + printResults(results) allCidrs := root.getAllV4CidrsString(false) assert.Equal(t, len(allCidrs), 24-16+1) @@ -176,8 +181,8 @@ func TestSubConflictEqualPriority(t *testing.T) { _, super, _ := net.ParseCIDR("192.168.0.0/16") _, sub, _ := net.ParseCIDR("192.168.1.1/24") - root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) - root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(sub.String())}) + root.InsertCidr(super, &Metadata{Priority: []uint8{0}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) + root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) allCidrs := root.getAllV4CidrsString(false) @@ -197,8 +202,8 @@ func TestSubConflictEqualPriority(t *testing.T) { } func TestSuperConflictLowPriority(t *testing.T) { root := NewSupernet() - _, super, _ := net.ParseCIDR("192.168.0.0/16") _, sub, _ := net.ParseCIDR("192.168.1.1/24") + _, super, _ := net.ParseCIDR("192.168.0.0/16") root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(sub.String())}) root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) @@ -225,8 +230,8 @@ func TestSuperConflictHighPriority(t *testing.T) { _, super, _ := net.ParseCIDR("192.168.0.0/16") _, sub, _ := net.ParseCIDR("192.168.1.1/24") - root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(sub.String())}) - root.InsertCidr(super, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(super.String())}) + root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) + root.InsertCidr(super, &Metadata{Priority: []uint8{1}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) allCidrs := root.getAllV4CidrsString(false) @@ -242,8 +247,8 @@ func TestSuperConflictEqualPriority(t *testing.T) { _, super, _ := net.ParseCIDR("192.168.0.0/16") _, sub, _ := net.ParseCIDR("192.168.1.1/24") - root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(sub.String())}) - result := root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) + root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) + result := root.InsertCidr(super, &Metadata{Priority: []uint8{0}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) printResults(result) allCidrs := root.getAllV4CidrsString(false) @@ -305,79 +310,77 @@ func TestLookIPv6(t *testing.T) { } -func TestEqualConflictResults(t *testing.T) { - root := NewSupernet() - _, cidr1, _ := net.ParseCIDR("192.168.1.1/24") - _, cidr2, _ := net.ParseCIDR("192.168.1.1/24") +// func TestEqualConflictResults(t *testing.T) { +// root := NewSupernet() +// _, cidr1, _ := net.ParseCIDR("192.168.1.1/24") +// _, cidr2, _ := net.ParseCIDR("192.168.1.1/24") - results := root.InsertCidr(cidr1, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(cidr1.String())}) +// results := root.InsertCidr(cidr1, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(cidr1.String())}) - assert.Equal(t, len(results), 1) - assert.Equal(t, cidr1.String(), results[0].CIDR.String()) - assert.Equal(t, NONE, results[0].ConflictType) - assert.Equal(t, NO_ACTION, results[0].ResolutionAction) +// assert.Equal(t, len(results), 1) +// assert.Equal(t, cidr1.String(), results[0].CIDR.String()) +// assert.Equal(t, NONE, results[0].ConflictType) +// assert.Equal(t, INSERT_NEW_CIDR, results[0].ResolutionAction) - results = root.InsertCidr(cidr2, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(cidr2.String())}) +// results = root.InsertCidr(cidr2, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(cidr2.String())}) - assert.Equal(t, results[0].ConflictType, EQUAL_CIDR) - assert.Equal(t, results[0].ResolutionAction, REMOVE_EXISTING_CIDR) - assert.Equal(t, len(results), 1) - assert.Equal(t, cidr2.String(), results[0].CIDR.String()) +// assert.Equal(t, results[0].ConflictType, EQUAL_CIDR) +// assert.Equal(t, results[0].ResolutionAction, REMOVE_EXISTING_CIDR) +// assert.Equal(t, len(results), 1) +// assert.Equal(t, cidr2.String(), results[0].CIDR.String()) - assert.Equal(t, NodeToCidr(&(results[0].RemovedCIDRs[0])), cidr1.String()) - assert.Equal(t, NodeToCidr(&(results[0].AddedCIDRs[0])), cidr2.String()) -} +// assert.Equal(t, NodeToCidr(&(results[0].RemovedCIDRs[0])), cidr1.String()) +// assert.Equal(t, NodeToCidr(&(results[0].AddedCIDRs[0])), cidr2.String()) +// } -func TestSubConflictResults(t *testing.T) { - root := NewSupernet() - _, super, _ := net.ParseCIDR("192.168.0.0/16") - _, sub, _ := net.ParseCIDR("192.168.1.1/24") +// func TestSubConflictResults(t *testing.T) { +// root := NewSupernet() +// _, super, _ := net.ParseCIDR("192.168.0.0/16") +// _, sub, _ := net.ParseCIDR("192.168.1.1/24") - results := root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) +// results := root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) - assert.Equal(t, len(results), 1) - assert.Equal(t, super.String(), results[0].CIDR.String()) - assert.Equal(t, NONE, results[0].ConflictType) - assert.Equal(t, NO_ACTION, results[0].ResolutionAction) +// assert.Equal(t, len(results), 1) +// assert.Equal(t, super.String(), results[0].CIDR.String()) +// assert.Equal(t, NONE, results[0].ConflictType) +// assert.Equal(t, INSERT_NEW_CIDR, results[0].ResolutionAction) - results = root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(sub.String())}) - allCidrs := root.getAllV4CidrsString(false) - printPaths(root) - printResults(results) - assert.Equal(t, results[0].ConflictType, SUBCIDR) - assert.Equal(t, results[0].ResolutionAction, SPLIT_EXISTING_CIDR) - assert.Equal(t, len(results), 1) - assert.Equal(t, sub.String(), results[0].CIDR.String()) +// results = root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(sub.String())}) +// allCidrs := root.getAllV4CidrsString(false) +// printPaths(root) +// printResults(results) +// assert.Equal(t, results[0].ConflictType, SUBCIDR) +// assert.Equal(t, results[0].ResolutionAction, SPLIT_EXISTING_CIDR) +// assert.Equal(t, len(results), 1) +// assert.Equal(t, sub.String(), results[0].CIDR.String()) - assert.Equal(t, len(results[0].AddedCIDRs), len(allCidrs)) - assert.Equal(t, len(results[0].RemovedCIDRs), 1) +// assert.Equal(t, len(results[0].AddedCIDRs), len(allCidrs)) +// assert.Equal(t, len(results[0].RemovedCIDRs), 1) -} +// } func TestSuperConflictResults(t *testing.T) { root := NewSupernet() - _, super, _ := net.ParseCIDR("192.168.0.0/16") _, sub, _ := net.ParseCIDR("192.168.1.1/24") + _, super, _ := net.ParseCIDR("192.168.0.0/16") - results := root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) - - assert.Equal(t, len(results), 1) - assert.Equal(t, sub.String(), results[0].CIDR.String()) - assert.Equal(t, NONE, results[0].ConflictType) - assert.Equal(t, NO_ACTION, results[0].ResolutionAction) - - results = root.InsertCidr(super, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(super.String())}) + results := root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, originCIDR: sub, Attributes: makeCidrAtrr(super.String())}) - assert.Equal(t, results[0].ConflictType, SUPERCIDR) - assert.Equal(t, results[0].ResolutionAction, REMOVE_EXISTING_CIDR) - assert.Equal(t, 1, len(results), "it should have one result") - assert.Equal(t, super.String(), results[0].CIDR.String()) + assert.Equal(t, len(results.actions), 1) + assert.Equal(t, sub.String(), results.CIDR.String()) + assert.Equal(t, NoConflict{}, results.ConflictType) + assert.Equal(t, InsertNewCIDR{}, results.actions[0].Action) - assert.Equal(t, 1, len(results[0].AddedCIDRs), "Added CIDR must be 1") - assert.Equal(t, 1, len(results[0].RemovedCIDRs), "Removed CIDR must be 1") + results = root.InsertCidr(super, &Metadata{Priority: []uint8{1}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) + printResults(results) + assert.Equal(t, results.ConflictType, SuperCIDR{}) + assert.Equal(t, results.actions[0].Action, RemoveExistingCIDR{}) + assert.Equal(t, results.actions[1].Action, InsertNewCIDR{}) + assert.Equal(t, 2, len(results.actions), "it should have 2 actions") + assert.Equal(t, super.String(), results.CIDR.String()) - assert.Equal(t, super.String(), NodeToCidr(&(results[0].AddedCIDRs[0])), "Added CIDR must be 1") - assert.Equal(t, sub.String(), NodeToCidr(&(results[0].RemovedCIDRs[0])), "Removed CIDR must be 1") + assert.Equal(t, 1, len(results.actions[1].AddedCidrs), "Added CIDR must be 1") + assert.Equal(t, 1, len(results.actions[0].RemoveCidrs), "Removed CIDR must be 1") } @@ -386,24 +389,24 @@ func TestSuperConflictResultsWithSplit(t *testing.T) { _, super, _ := net.ParseCIDR("192.168.0.0/16") _, sub, _ := net.ParseCIDR("192.168.1.1/24") - results := root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(sub.String())}) + results := root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) - assert.Equal(t, len(results), 1) - assert.Equal(t, sub.String(), results[0].CIDR.String()) - assert.Equal(t, NONE, results[0].ConflictType) - assert.Equal(t, NO_ACTION, results[0].ResolutionAction) + assert.Equal(t, len(results.actions), 1) + assert.Equal(t, sub.String(), results.CIDR.String()) + assert.Equal(t, NoConflict{}, results.ConflictType) + assert.Equal(t, InsertNewCIDR{}, results.actions[0].Action) - results = root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) + results = root.InsertCidr(super, &Metadata{Priority: []uint8{0}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) printResults(results) - assert.Equal(t, results[0].ConflictType, SUPERCIDR) - assert.Equal(t, results[0].ResolutionAction, SPLIT_INSERTED_CIDR) - assert.Equal(t, 1, len(results), "it should have one result") + assert.Equal(t, results.ConflictType, SuperCIDR{}) + assert.Equal(t, results.actions[0].Action, SplitInsertedCIDR{}) + assert.Equal(t, 1, len(results.actions), "it should have one result") - assert.Equal(t, 8, len(results[0].AddedCIDRs), "Added CIDR must be 8") - assert.Equal(t, 0, len(results[0].RemovedCIDRs), "Removed CIDR must be 0") + assert.Equal(t, 8, len(results.actions[0].AddedCidrs), "Added CIDR must be 8") + assert.Equal(t, 0, len(results.actions[0].RemoveCidrs), "Removed CIDR must be 0") addedCidrs := []string{} - for _, added := range results[0].AddedCIDRs { + for _, added := range results.actions[0].AddedCidrs { addedCidrs = append(addedCidrs, NodeToCidr(&added)) } @@ -436,11 +439,11 @@ func TestNestedConflictResolution1(t *testing.T) { for _, deepCidr := range deepCidrs { _, ipnet, _ := net.ParseCIDR(deepCidr.cidr) - results := root.InsertCidr(ipnet, &Metadata{Priority: deepCidr.priorities, Attributes: makeCidrAtrr(deepCidr.cidr)}) + results := root.InsertCidr(ipnet, &Metadata{Priority: deepCidr.priorities, originCIDR: ipnet, Attributes: makeCidrAtrr(deepCidr.cidr)}) printResults(results) printPaths(root) } - results := root.InsertCidr(super, &Metadata{Priority: []uint8{2}, Attributes: makeCidrAtrr(super.String())}) + results := root.InsertCidr(super, &Metadata{Priority: []uint8{2}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) printResults(results) printPaths(root) // THIS TEST IS A BIT NOSY, BLGTM @@ -491,11 +494,11 @@ func TestNestedConflictResolution2(t *testing.T) { for _, deepCidr := range deepCidrs { _, ipnet, _ := net.ParseCIDR(deepCidr.cidr) - results := root.InsertCidr(ipnet, &Metadata{Priority: deepCidr.priorities, Attributes: makeCidrAtrr(deepCidr.cidr)}) + results := root.InsertCidr(ipnet, &Metadata{Priority: deepCidr.priorities, originCIDR: ipnet, Attributes: makeCidrAtrr(deepCidr.cidr)}) printResults(results) printPaths(root) } - results := root.InsertCidr(super, &Metadata{Priority: []uint8{2}, Attributes: makeCidrAtrr(super.String())}) + results := root.InsertCidr(super, &Metadata{Priority: []uint8{2}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) printResults(results) printPaths(root) // THIS TEST IS A BIT NOSY, BLGTM @@ -540,7 +543,7 @@ func TestRemoveBranchWithLimit(t *testing.T) { for _, deepCidr := range deepCidrs { _, ipnet, _ := net.ParseCIDR(deepCidr.cidr) - results := root.InsertCidr(ipnet, &Metadata{Priority: deepCidr.priorities, Attributes: makeCidrAtrr(deepCidr.cidr)}) + results := root.InsertCidr(ipnet, &Metadata{Priority: deepCidr.priorities, originCIDR: ipnet, Attributes: makeCidrAtrr(deepCidr.cidr)}) printResults(results) printPaths(root) } @@ -554,21 +557,16 @@ func makeCidrAtrr(cidr string) map[string]string { return attr } -func printResults(results []*InsertionResult) { - for _, result := range results { - fmt.Println(result.String()) - } +func printResults(results *InsertionResult) { + fmt.Println(results.String()) } func printPaths(root *Supernet) { for _, node := range root.ipv4Cidrs.GetLeafs() { - if node.Metadata != nil { - fmt.Printf("%v [%s] -> from [%s]\n", node.GetPath(), bitsToCidr(node.GetPath(), false).String(), node.Metadata.Attributes["cidr"]) + if node.Metadata() != nil { + fmt.Printf("%v [%s] -> from [%s]\n", node.GetPath(), BitsToCidr(node.GetPath(), false).String(), node.Metadata().Attributes["cidr"]) } else { - // hack - - fmt.Printf("%v <-!!-- [%s] \n", node.GetPath(), bitsToCidr(node.GetPath(), false).String()) - // panic("Path node has not been cleaned") + fmt.Printf("%v <-!!-- [%s] \n", node.GetPath(), BitsToCidr(node.GetPath(), false).String()) } } } diff --git a/pkg/supernet/uitls.go b/pkg/supernet/uitls.go new file mode 100644 index 0000000..1f3f04a --- /dev/null +++ b/pkg/supernet/uitls.go @@ -0,0 +1,138 @@ +package supernet + +import ( + "net" + + "github.com/khalid_nowaf/supernet/pkg/trie" +) + +// BitsToCidr converts a slice of binary bits into a net.IPNet structure that represents a CIDR. +// This is used to form the IP address and subnet mask from a binary representation. +// +// Parameters: +// - bits: A slice of integers (0 or 1) representing the binary form of the IP address. +// - ipV6: A boolean flag indicating whether the address is IPv6 (true) or IPv4 (false). +// +// Returns: +// - A pointer to a net.IPNet structure that includes both the IP address and the subnet mask. +// +// This function dynamically constructs the IP and mask based on the length of the bits slice and the type of IP (IPv4 or IPv6). +// It supports a flexible number of bits and automatically adjusts for IPv4 (up to 32 bits) and IPv6 (up to 128 bits). +// +// Example: +// +// For a bits slice representing "192.168.1.1" and ipV6 set to false, the function would return an IPNet with the IP "192.168.1.1" +// and a full subnet mask "255.255.255.255" if all bits are provided. +func BitsToCidr(bits []int, ipV6 bool) *net.IPNet { + maxBytes := 4 + if ipV6 { + maxBytes = 16 // Set the byte limit to 16 for IPv6 + } + + ipBytes := make([]byte, 0, maxBytes) + maskBytes := make([]byte, 0, maxBytes) + currentBit := 0 + bitsLen := len(bits) - 1 + + for iByte := 0; iByte < maxBytes; iByte++ { + var ipByte byte + var maskByte byte + for i := 0; i < 8; i++ { + if currentBit <= bitsLen { + ipByte = ipByte<<1 | byte(bits[currentBit]) + maskByte = maskByte<<1 | 1 // Add a bit to the mask for each bit processed + currentBit++ + } else { + ipByte = ipByte << 1 // Shift the byte to the left, filling with zeros + maskByte = maskByte << 1 + } + } + ipBytes = append(ipBytes, ipByte) + maskBytes = append(maskBytes, maskByte) + } + + return &net.IPNet{ + IP: net.IP(ipBytes), + Mask: net.IPMask(maskBytes), + } +} + +// NodeToCidr converts a given trie node into a CIDR (Classless Inter-Domain Routing) string representation. +// This function uses the node's path to generate the CIDR string. +// +// Parameters: +// - t: Pointer to a trie.BinaryTrie node of type Metadata. It must contain valid metadata and a path. +// - isV6: A boolean indicating whether the IP version is IPv6. True means IPv6, false means IPv4. +// +// Returns: +// - A string representing the CIDR notation of the node's IP address. +// +// Panics: +// - If the node's metadata is nil, indicating that it is a path node without associated CIDR data, +// this function will panic with a specific error message. +// +// Example: +// +// Given a trie node representing an IP address with metadata, this function will output the address in CIDR format, +// like "192.168.1.0/24" for IPv4 or "2001:db8::/32" for IPv6. +func NodeToCidr(t *trie.BinaryTrie[Metadata]) string { + if t.Metadata() == nil { + panic("[Bug] NodeToCidr: Cannot convert a trie path node to CIDR, metadata is missing") + } + // Convert the binary path of the trie node to CIDR format using the bitsToCidr function, + // then convert the resulting net.IPNet object to a string. + return BitsToCidr(t.GetPath(), t.Metadata().IsV6).String() +} + +// CidrToBits converts a net.IPNet object into a slice of integers representing the binary bits of the network address. +// Additionally, it returns the depth of the network mask. +// +// The function panics if: +// - ipnet is nil, indicating invalid input. +// - the network mask is /0, which is technically valid but not supported by this library. +// +// Parameters: +// - ipnet: Pointer to a net.IPNet object containing the IP address and the network mask. +// +// Returns: +// +// - A slice of integers representing the binary format of the IP address up to the length of the network mask. +// +// - An integer representing the number of bits in the network mask minus one. +// +// Example: +// For IP address "192.168.1.1/24", this function would return a slice with the first 24 bits of the address in binary form, +// and the number 23 as the depth. +func CidrToBits(ipnet *net.IPNet) ([]int, int) { + if ipnet == nil { + panic("[BUG] cidrToBits: IPNet is nil: validate the input before calling cidrToBits") + } + + maskSize, _ := ipnet.Mask.Size() + if maskSize == 0 { + panic("[BUG] cidrToBits: network Mask /0 not valid: " + ipnet.String()) + } + + path := make([]int, maskSize) + currentBit := 0 + + // Process each byte of the IP address to convert it into bits. + for _, byteVal := range ipnet.IP { + // Iterate over each bit in the byte. + for bitPosition := 0; bitPosition < 8; bitPosition++ { + // Shift the byte to the right to place the bit at the most significant position (leftmost), + // and mask it with 1 to isolate the bit. + bit := (byteVal >> (7 - bitPosition)) & 1 + path[currentBit] = int(bit) + + // If we have processed bits equal to the size of the network mask, return the result. + if currentBit == (maskSize - 1) { + return path, maskSize - 1 + } + currentBit++ + } + } + + // This line should not be reached; if it is, there is an error in bit calculation. + panic("[BUG] cidrToBits: bit calculation error - did not process enough bits for the mask size") +} diff --git a/pkg/trie/trie.go b/pkg/trie/trie.go index c279e71..b6cc9be 100644 --- a/pkg/trie/trie.go +++ b/pkg/trie/trie.go @@ -2,6 +2,19 @@ package trie import "fmt" +type Trie[T any] interface { + Parent() *Trie[T] + Metadata() *T + Pos() int + Depth() int + Child(pos int) *Trie[T] + Sibling() *Trie[T] + AttachChild(child *Trie[T], pos int) *Trie[T] + AttachSibling(sibling *Trie[T]) + Detach() + DetachBranch() +} + // is an alias for int used to define child positions in a trie node. type ChildPos = int @@ -11,9 +24,9 @@ const ONE ChildPos = 1 // BinaryTrie is a generic type representing a node in a trie. type BinaryTrie[T any] struct { - Parent *BinaryTrie[T] // Pointer to the parent node - Children [2]*BinaryTrie[T] // Array of pointers to child nodes - Metadata *T // Generic type to store additional information + parent *BinaryTrie[T] // Pointer to the parent node + children [2]*BinaryTrie[T] // Array of pointers to child nodes + metadata *T // Generic type to store additional information pos bool // Represents the potions value at this node's position in its parent (0 or 1) depth int // The depth of this node in the trie } @@ -21,7 +34,7 @@ type BinaryTrie[T any] struct { // NewTrieWithMetadata creates a new trie node with the provided metadata and initializes it. func NewTrieWithMetadata[T any](metadata *T) *BinaryTrie[T] { return &BinaryTrie[T]{ - Metadata: metadata, + metadata: metadata, depth: 0, } } @@ -34,71 +47,85 @@ func NewTrie() *BinaryTrie[string] { // isRoot checks if the current node is the root of the trie. func (t *BinaryTrie[T]) isRoot() bool { - return t.Parent == nil + return t.parent == nil } // returns 1 or 0. -func (t *BinaryTrie[T]) GetPos() int { +func (t *BinaryTrie[T]) Pos() int { if t.pos { return 1 } return 0 } -// GetDepth returns the depth of the node in the trie. -func (t *BinaryTrie[T]) GetDepth() int { +// Depth returns the depth of the node in the trie. +func (t *BinaryTrie[T]) Depth() int { return t.depth } -// adds a child at the specified position if no child exists there yet. -// return the new added child or the existing one -func (t *BinaryTrie[T]) AddChildAtIfNotExist(child *BinaryTrie[T], at ChildPos) *BinaryTrie[T] { - if t.Children[at] != nil { - return t.Children[at] +func (t *BinaryTrie[T]) Parent() *BinaryTrie[T] { + return t.parent +} + +func (t *BinaryTrie[T]) Metadata() *T { + return t.metadata +} + +func (t *BinaryTrie[T]) UpdateMetadata(newNewMetadata *T) { + t.metadata = newNewMetadata +} + +// attached a child at the specified position if no child exists there yet. +// return the new attached child or the existing one +func (t *BinaryTrie[T]) AttachChild(child *BinaryTrie[T], at ChildPos) *BinaryTrie[T] { + if t.children[at] != nil { + return t.children[at] } - return t.AddChildOrReplaceAt(child, at) + return t.ReplaceChild(child, at) } -// adds and return a child at the specified position, replacing any existing child. -// this potently will detach the the subtree of the child has children -func (t *BinaryTrie[T]) AddChildOrReplaceAt(child *BinaryTrie[T], at ChildPos) *BinaryTrie[T] { - child.Parent = t +// replacing any existing child, or simply attach the child +// if child is replace it will potentially will detach the the subtree of child +func (t *BinaryTrie[T]) ReplaceChild(child *BinaryTrie[T], at ChildPos) *BinaryTrie[T] { + child.parent = t child.pos = (at == ONE) child.depth = t.depth + 1 - t.Children[at] = child + t.children[at] = child return child } -func (t *BinaryTrie[T]) AddSibling(sibling *BinaryTrie[T]) *BinaryTrie[T] { - return t.Parent.AddChildAtIfNotExist(sibling, t.GetPos()^1) +// attach child as sibilating to the current node +func (t *BinaryTrie[T]) AttachSibling(sibling *BinaryTrie[T]) *BinaryTrie[T] { + return t.parent.AttachChild(sibling, t.Pos()^1) } // returns the child node Zero or One // -// node.GetChildAt(trie.Zero) -func (t *BinaryTrie[T]) GetChildAt(at ChildPos) *BinaryTrie[T] { +// Example: +// node.Child(trie.Zero) +func (t *BinaryTrie[T]) Child(at ChildPos) *BinaryTrie[T] { if t == nil { panic("[BUG] GetChildAt: struct must not be nil nil") } - return t.Children[at] + return t.children[at] } // it will return the sibling at the same level, or nil -func (t *BinaryTrie[T]) GetSibling() *BinaryTrie[T] { - return t.Parent.GetChildAt(t.GetPos() ^ 1) +func (t *BinaryTrie[T]) Sibling() *BinaryTrie[T] { + return t.parent.Child(t.Pos() ^ 1) } func (t *BinaryTrie[T]) IsBranch() bool { - return t.GetSibling() != nil + return t.Sibling() != nil } // Detach will discount the node from the tree // if there is no reference to the node, it will be GC'ed func (t *BinaryTrie[T]) Detach() { if !t.isRoot() { - t.Parent.Children[t.GetPos()] = nil + t.parent.children[t.Pos()] = nil } else { panic("[BUG] Detach: You can not Detach the root") } @@ -107,7 +134,7 @@ func (t *BinaryTrie[T]) Detach() { // removes current node, and the whole branch // and return the parent of last removed node // this will remove any parent that had only one child until it -// reaches a parant that have 2 children (beginning of the branch) +// reaches a parent that have 2 children (beginning of the branch) // node(branch) -->node-->node-->node // // l>node-->node-->node (Dutch) @@ -121,8 +148,8 @@ func (t *BinaryTrie[T]) DetachBranch(limit int) *BinaryTrie[T] { } nearestBranchedNode := t t.ForEachStepUp(func(next *BinaryTrie[T]) { - if !next.Parent.isRoot() { - nearestBranchedNode = next.Parent + if !next.parent.isRoot() { + nearestBranchedNode = next.parent } }, func(next *BinaryTrie[T]) bool { @@ -131,22 +158,22 @@ func (t *BinaryTrie[T]) DetachBranch(limit int) *BinaryTrie[T] { nearestBranchedNode.Detach() - return nearestBranchedNode.Parent + return nearestBranchedNode.parent } // checks if the node is a leaf (has no children). func (t *BinaryTrie[T]) IsLeaf() bool { - return t.Children[0] == nil && t.Children[1] == nil + return t.children[0] == nil && t.children[1] == nil } // applies a function to each non-nil child of the node. // will return the original node t func (t *BinaryTrie[T]) ForEachChild(f func(t *BinaryTrie[T])) *BinaryTrie[T] { - if t.Children[0] != nil { - f(t.Children[0]) + if t.children[0] != nil { + f(t.children[0]) } - if t.Children[1] != nil { - f(t.Children[1]) + if t.children[1] != nil { + f(t.children[1]) } return t } @@ -175,9 +202,9 @@ func (t *BinaryTrie[T]) forEachStepDown(f func(t *BinaryTrie[T]), while func(t * // will return the original node t func (t *BinaryTrie[T]) ForEachStepUp(f func(*BinaryTrie[T]), while func(*BinaryTrie[T]) bool) *BinaryTrie[T] { current := t - for current.Parent != nil && (while == nil || while(current)) { + for current.parent != nil && (while == nil || while(current)) { f(current) - current = current.Parent + current = current.parent } return t } @@ -189,14 +216,14 @@ func (t *BinaryTrie[T]) GetPath() []int { path := []int{} t.ForEachStepUp(func(tr *BinaryTrie[T]) { - path = append([]int{tr.GetPos()}, path...) + path = append([]int{tr.Pos()}, path...) }, nil) return path } -// TODO: Doc This +// return all the leafs on the tree func (t *BinaryTrie[T]) GetLeafs() []*BinaryTrie[T] { leafs := []*BinaryTrie[T]{} t.ForEachStepDown(func(t *BinaryTrie[T]) { diff --git a/pkg/trie/trie_test.go b/pkg/trie/trie_test.go index 7fdc44d..21386d6 100644 --- a/pkg/trie/trie_test.go +++ b/pkg/trie/trie_test.go @@ -13,45 +13,45 @@ import ( func TestNewTrie(t *testing.T) { root := NewTrie() assert.NotNil(t, root, "Trie should not be nil upon creation") - assert.Empty(t, *root.Metadata, "Metadata should be initialized to nil for a new boolean Trie") - assert.Equal(t, 0, root.GetDepth(), "Depth should be initialized to 0 for a new Trie") + assert.Empty(t, *root.metadata, "Metadata should be initialized to nil for a new boolean Trie") + assert.Equal(t, 0, root.Depth(), "Depth should be initialized to 0 for a new Trie") } // TestNewTrieWithMetadata verifies the initialization of a Trie node with specific metadata. func TestNewTrieWithMetadata(t *testing.T) { root := NewTrie() assert.NotNil(t, root, "Trie should not be nil upon creation") - assert.Equal(t, "", *root.Metadata, "Metadata should match the initialization value") - assert.Equal(t, 0, root.GetDepth(), "Depth should be initialized to 0 for a new Trie") + assert.Equal(t, "", *root.metadata, "Metadata should match the initialization value") + assert.Equal(t, 0, root.Depth(), "Depth should be initialized to 0 for a new Trie") } // TestAddChildAtIfNotExist verifies the behavior of adding a child node if it does not already exist. func TestAddChildAtIfNotExist(t *testing.T) { root := NewTrie() child := NewTrie() - addedChild := root.AddChildAtIfNotExist(child, ONE) + addedChild := root.AttachChild(child, ONE) assert.Equal(t, child, addedChild, "Should return the added child") - assert.Equal(t, root, child.Parent, "Child's parent should be set correctly") - assert.Equal(t, 1, child.GetPos(), "Child's binary value should be true for position ONE") - assert.Equal(t, 1, child.GetDepth(), "Child's depth should increment by 1 from the parent") + assert.Equal(t, root, child.parent, "Child's parent should be set correctly") + assert.Equal(t, 1, child.Pos(), "Child's binary value should be true for position ONE") + assert.Equal(t, 1, child.Depth(), "Child's depth should increment by 1 from the parent") } // TestGetChildAt verifies retrieving children from specific positions. func TestGetChildAt(t *testing.T) { root := NewTrie() child := NewTrie() - root.AddChildAtIfNotExist(child, ZERO) + root.AttachChild(child, ZERO) - assert.Equal(t, child, root.GetChildAt(ZERO), "Should retrieve the child at position ZERO") - assert.Nil(t, root.GetChildAt(ONE), "Should return nil for an empty child position") + assert.Equal(t, child, root.Child(ZERO), "Should retrieve the child at position ZERO") + assert.Nil(t, root.Child(ONE), "Should return nil for an empty child position") } // TestForEachChild checks that ForEachChild iterates over all children correctly. func TestForEachChild(t *testing.T) { root := NewTrie() - root.AddChildAtIfNotExist(NewTrie(), ZERO) - root.AddChildAtIfNotExist(NewTrie(), ONE) + root.AttachChild(NewTrie(), ZERO) + root.AttachChild(NewTrie(), ONE) var count int root.ForEachChild(func(t *BinaryTrie[string]) { @@ -73,7 +73,7 @@ func TestForEachStepDown(t *testing.T) { // Mark each visited node with "visited" in its metadata. root.ForEachStepDown(func(tr *BinaryTrie[string]) { - tr.Metadata = strPtr("visited") + tr.metadata = strPtr("visited") }, nil) traverseAndVerify = func(tr *BinaryTrie[string]) { @@ -81,8 +81,8 @@ func TestForEachStepDown(t *testing.T) { return } tr.ForEachChild(func(c *BinaryTrie[string]) { - visitedPaths += strconv.Itoa(c.GetPos()) - assert.Contains(t, *c.Metadata, "visited", "Metadata should contain 'visited'") + visitedPaths += strconv.Itoa(c.Pos()) + assert.Contains(t, *c.metadata, "visited", "Metadata should contain 'visited'") traverseAndVerify(c) }) } @@ -93,8 +93,8 @@ func TestForEachStepDown(t *testing.T) { // TestGetPath verifies that the path from the root to a specific node is correctly identified. func TestGetPath(t *testing.T) { root := NewTrie() - child := root.AddChildAtIfNotExist(NewTrie(), ONE) - grandchild := child.AddChildAtIfNotExist(NewTrie(), ZERO) + child := root.AttachChild(NewTrie(), ONE) + grandchild := child.AttachChild(NewTrie(), ZERO) path := grandchild.GetPath() expectedPath := []int{1, 0} @@ -124,9 +124,9 @@ func TestGetSibling(t *testing.T) { generateTrieAs(paths, root) leafs := root.GetLeafs() - assert.NotNil(t, leafs[0].GetSibling()) - assert.Equal(t, 1, leafs[0].GetSibling().GetPos()) - assert.Nil(t, leafs[1].GetSibling()) + assert.NotNil(t, leafs[0].Sibling()) + assert.Equal(t, 1, leafs[0].Sibling().Pos()) + assert.Nil(t, leafs[1].Sibling()) } func TestAddSiblingIfNotExist(t *testing.T) { @@ -135,11 +135,11 @@ func TestAddSiblingIfNotExist(t *testing.T) { generateTrieAs(paths, root) leafs := root.GetLeafs() - assert.NotNil(t, leafs[0].GetSibling()) - assert.Nil(t, leafs[1].GetSibling()) + assert.NotNil(t, leafs[0].Sibling()) + assert.Nil(t, leafs[1].Sibling()) sibling := NewTrie() - leafs[1].AddSibling(sibling) - assert.Equal(t, sibling, leafs[1].GetSibling()) + leafs[1].AttachSibling(sibling) + assert.Equal(t, sibling, leafs[1].Sibling()) } @@ -149,10 +149,10 @@ func TestAddSiblingIfExist(t *testing.T) { generateTrieAs(paths, root) leafs := root.GetLeafs() - assert.NotNil(t, leafs[0].GetSibling()) + assert.NotNil(t, leafs[0].Sibling()) sibling := NewTrie() - leafs[0].AddSibling(sibling) - assert.NotEqual(t, sibling, leafs[0].GetSibling()) + leafs[0].AttachSibling(sibling) + assert.NotEqual(t, sibling, leafs[0].Sibling()) } func TestDetach(t *testing.T) { @@ -227,7 +227,7 @@ func BenchmarkWrites32BitPaths(b *testing.B) { for _, path := range paths { for _, pos := range path { - root.AddChildAtIfNotExist(NewTrie(), pos) + root.AttachChild(NewTrie(), pos) } } @@ -246,7 +246,7 @@ func BenchmarkRead32BitPaths(b *testing.B) { root := NewTrie() for _, path := range paths { for _, pos := range path { - root.AddChildAtIfNotExist(NewTrie(), pos) + root.AttachChild(NewTrie(), pos) } } @@ -264,7 +264,7 @@ func BenchmarkRead32BitPaths(b *testing.B) { panic("node is nil") } pr = node - node = node.GetChildAt(pos) + node = node.Child(pos) } } @@ -275,7 +275,7 @@ func BenchmarkGetLeafPaths(b *testing.B) { root := NewTrie() for _, path := range paths { for _, pos := range path { - root.AddChildAtIfNotExist(NewTrie(), pos) + root.AttachChild(NewTrie(), pos) } } @@ -289,11 +289,11 @@ func generateTrieAs(paths []string, trie *BinaryTrie[string]) { for _, path := range paths { current := trie for _, bit := range path { - metadata := strPtr(*current.Metadata + string(bit) + " -> ") + metadata := strPtr(*current.metadata + string(bit) + " -> ") if bit == '0' { - current = current.AddChildAtIfNotExist(NewTrieWithMetadata(metadata), ZERO) + current = current.AttachChild(NewTrieWithMetadata(metadata), ZERO) } else { - current = current.AddChildAtIfNotExist(NewTrieWithMetadata(metadata), ONE) + current = current.AttachChild(NewTrieWithMetadata(metadata), ONE) } } } diff --git a/supernet.go b/supernet.go deleted file mode 100644 index cc031c4..0000000 --- a/supernet.go +++ /dev/null @@ -1,680 +0,0 @@ -package supernet - -import ( - "fmt" - "net" - "strings" - - "github.com/khalid_nowaf/supernet/pkg/trie" -) - -// ConflictType defines the types of conflicts that may arise during the insertion of new CIDRs into a trie structure. -type ConflictType int - -const ( - EQUAL_CIDR ConflictType = iota // a conflict where the new CIDR exactly matches an existing CIDR in the trie. - SUBCIDR // the new CIDR is a subrange of an existing CIDR in the trie. - SUPERCIDR // the new CIDR encompasses one or more existing CIDRs in the trie. - NONE // no conflict with existing CIDRs in the trie -) - -func (c ConflictType) String() string { - switch c { - case EQUAL_CIDR: - return "EQUAL CIDR" - case SUBCIDR: - return "SUBCIDR" - case SUPERCIDR: - return "SUPERCIDR" - case NONE: - return "NONE" - default: - return "UNKNOWN CONFLICT TYPE" - } -} - -// ResolutionAction defines the possible actions to resolve a conflict between CIDRs in a trie. -type ResolutionAction int - -const ( - IGNORE_INSERTION ResolutionAction = iota // no action is required to resolve the conflict. - SPLIT_INSERTED_CIDR // inserted CIDR should be split to resolve the conflict. - SPLIT_EXISTING_CIDR // existing CIDR should be split to resolve the conflict. - REMOVE_EXISTING_CIDR // existing CIDR should be removed to resolve the conflict. - NO_ACTION // no action is taken because there is no conflict -) - -func (r ResolutionAction) String() string { - switch r { - case IGNORE_INSERTION: - return "IGNORE INSERTION" - case SPLIT_INSERTED_CIDR: - return "SPLIT INSERTED CIDR" - case SPLIT_EXISTING_CIDR: - return "SPLIT EXISTING CIDR" - case REMOVE_EXISTING_CIDR: - return "REMOVE EXISTING CIDR" - case NO_ACTION: - return "NO ACTION" - default: - return "UNKNOWN ACTION" - } -} - -// records the outcome of attempting to insert a CIDR for reporting -type InsertionResult struct { - CIDR net.IPNet // CIDR was attempted to be inserted. - ConflictType ConflictType // type of conflict encountered during the insertion. - ResolutionAction ResolutionAction // action taken to resolve the conflict. - ConflictedWith trie.BinaryTrie[Metadata] // conflicted CIDRS with the inserted node, if any. - AddedCIDRs []trie.BinaryTrie[Metadata] // added CIDRS from the resolution/insertion process. - RemovedCIDRs []trie.BinaryTrie[Metadata] // removed CIDRS from the resolution process. -} - -// take a copy appends a new CIDR trie node to the ResultedAddedCIDRs -// -// to keep track of all the added CIDRs from resolving a conflict. -func (ir *InsertionResult) appendAddedCidr(cidr *trie.BinaryTrie[Metadata]) { - ir.AddedCIDRs = append(ir.AddedCIDRs, *cidr) -} - -// take copy and appends a removed existing CIDR to the ResultedCIDRs -// to keep track of all removed CIDRs from resolving a conflict. -func (ir *InsertionResult) appendRemovedCidr(cidr *trie.BinaryTrie[Metadata]) { - ir.RemovedCIDRs = append(ir.RemovedCIDRs, *cidr) -} - -func (ir *InsertionResult) String() string { - addedCidrs := []string{} - removedCidrs := []string{} - - for _, added := range ir.AddedCIDRs { - addedCidrs = append(addedCidrs, NodeToCidr(&added)) - } - for _, removed := range ir.RemovedCIDRs { - removedCidrs = append(removedCidrs, NodeToCidr(&removed)) - } - if ir.ConflictType != NONE { - return fmt.Sprintf(` - Conflict Detected %v | `+ - `CIDR (%s) conflicted with (%s)%v | `+ - `Action Taken: %v | `+ - `Added CIDRs: %v | `+ - `Remove CIDRs: %v - `, ir.ConflictType.String(), ir.CIDR.String(), NodeToCidr(&ir.ConflictedWith), ir.ConflictedWith.Metadata.Priority, ir.ResolutionAction.String(), addedCidrs, removedCidrs) - } else { - return fmt.Sprintf("New CIDR (%s) Inserted\n", ir.CIDR.String()) - } -} - -// BuildNewResult creates a new InsertResult that contains a shallow copy of the original -// InsertResult's CIDR, ConflictType, and ResolutionAction. It does not include ConflictedWith -// or ResultedCIDRs as these may not be relevant to the copied context. -func BuildNewResult(cidr net.IPNet, ct ConflictType, ra ResolutionAction, cw trie.BinaryTrie[Metadata]) *InsertionResult { - return &InsertionResult{ - CIDR: cidr, - ConflictType: ct, - ResolutionAction: ra, - ConflictedWith: cw, - } -} - -// holds the properties for a CIDR node within a trie, including IP version, priority, and additional attributes. -// -// Properties: -// - isV6: True if the CIDR is an IPv6 address -// - Priority: An array of uint8 representing the priority of the CIDR which aids in conflict resolution. -// - Attributes: A map of string keys to string values providing additional information about the CIDR. -type Metadata struct { - IsV6 bool - Priority []uint8 - Attributes map[string]string -} - -// creates a Metadata instance with default values. -// -// Returns: -// - A pointer to a Metadata instance initialized with default values. -func NewDefaultMetadata() *Metadata { - return &Metadata{} -} - -// Supernet represents a structure containing both IPv4 and IPv6 CIDRs, each stored in a separate trie. -type Supernet struct { - ipv4Cidrs *trie.BinaryTrie[Metadata] - ipv6Cidrs *trie.BinaryTrie[Metadata] -} - -// initializes a new supernet instance with separate tries for IPv4 and IPv6 CIDRs. -// -// Returns: -// - A pointer to a newly initialized supernet instance. -func NewSupernet() *Supernet { - return &Supernet{ - ipv4Cidrs: &trie.BinaryTrie[Metadata]{}, - ipv6Cidrs: &trie.BinaryTrie[Metadata]{}, - } -} - -// newPathTrie creates a new trie node intended for path utilization without any associated metadata. -// -// Returns: -// - A pointer to a newly created trie.BinaryTrie node with no metadata. -func newPathTrie() *trie.BinaryTrie[Metadata] { - return &trie.BinaryTrie[Metadata]{} -} - -// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. -// -// Parameters: -// - forV6: A boolean flag if th CIDR is IPv6 -// -// Returns: -// - A slice of TrieNode, each representing a CIDR in the specified trie. -func (super *Supernet) GetAllV4Cidrs(forV6 bool) []*trie.BinaryTrie[Metadata] { - supernet := super.ipv4Cidrs - if forV6 { - supernet = super.ipv6Cidrs - } - return supernet.GetLeafs() -} - -// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. -// -// Parameters: -// - forV6: A boolean flag if th CIDR is IPv6 -// -// Returns: -// - A slice of strings, each representing a CIDR in the specified trie. -func (super *Supernet) getAllV4CidrsString(forV6 bool) []string { - supernet := super.ipv4Cidrs - if forV6 { - supernet = super.ipv6Cidrs - } - var cidrs []string - for _, node := range supernet.GetLeafs() { - cidrs = append(cidrs, bitsToCidr(node.GetPath(), forV6).String()) - } - return cidrs -} - -// insertInit prepares the necessary nodes and metadata for inserting a new CIDR into the supernet. -// -// Parameters: -// - ipnet: net.IPNet representing the CIDR to be inserted. -// - metadata: Metadata associated with the CIDR. If nil, default metadata is used. -// -// Returns: -// - currentNode: The root node of the appropriate trie (IPv4 or IPv6) based on the IP address in ipnet. -// - newCidrNode: A new trie node initialized with the metadata intended for the new CIDR. -// - path: A slice of integers representing the binary path derived from the CIDR. -// - depth: The depth in the trie at which the CIDR should be inserted, determined by the number of bits in the CIDR's netmask. -func (super *Supernet) insertInit(ipnet *net.IPNet, metadata *Metadata) ( - currentNode *trie.BinaryTrie[Metadata], - newCidrNode *trie.BinaryTrie[Metadata], - path []int, - depth int, - insertedResult *InsertionResult, -) { - insertedResult = &InsertionResult{} - // Create a copy of the provided metadata or initialize it with defaults if nil. - copyMetadata := metadata - if copyMetadata == nil { - copyMetadata = NewDefaultMetadata() - } - - // Determine the appropriate supernet (IPv4 or IPv6) based on the IP address format. - if ipnet.IP.To4() != nil { - // IPv4 CIDR. - currentNode = super.ipv4Cidrs - } else if ipnet.IP.To16() != nil { - // IPv6 CIDR. - currentNode = super.ipv6Cidrs - copyMetadata.IsV6 = true // Ensure metadata reflects the IP version. - } - - // Initialize a new trie node with the copied or default metadata. - newCidrNode = trie.NewTrieWithMetadata(copyMetadata) - - // Convert the CIDR to a binary path and calculate the depth. - path, depth = cidrToBits(ipnet) - - // we add cidrs mask as the last priority - // if two conflicted CIDRs has the same priority - // we favor the smaller CIDR - copyMetadata.Priority = append(copyMetadata.Priority, uint8(depth)) - - // init InsertedResult with defaults (happy path) - insertedResult.ResolutionAction = NO_ACTION - insertedResult.ConflictType = NONE - insertedResult.CIDR = *ipnet - - return currentNode, newCidrNode, path, depth, insertedResult -} - -// InsertCidr attempts to insert a new CIDR into the supernet, handling conflicts according to predefined priorities. -// It traverses through the trie, adding new nodes as needed and resolving conflicts when they occur. -// -// Parameters: -// - ipnet: net.IPNet, the CIDR to be inserted. -// - metadata: Metadata associated with the CIDR, used for conflict resolution and node creation. -// -// This function navigates through each bit of the new CIDR's path, trying to add a new node if it doesn't already exist, -// and handles various types of conflicts (EQUAL_CIDR, SUBCIDR, SUPERCIDR) by comparing the priorities of the involved CIDRs. - -func (super *Supernet) InsertCidr(ipnet *net.IPNet, metadata *Metadata) []*InsertionResult { - currentNode, newCidrNode, path, depth, insertedResult := super.insertInit(ipnet, metadata) - var supernetToSplitLater *trie.BinaryTrie[Metadata] - - for currentDepth, bit := range path { - currentNode = currentNode.AddChildAtIfNotExist(newPathTrie(), bit) - - switch isThereAConflict(currentNode, depth) { - case EQUAL_CIDR: - insertedResult.ConflictType = EQUAL_CIDR - insertedResult.ConflictedWith = *currentNode - - if comparator(newCidrNode, currentNode) { - insertedResult.ResolutionAction = REMOVE_EXISTING_CIDR - insertedResult.appendRemovedCidr(currentNode) - - currentNode = currentNode.Parent.AddChildOrReplaceAt(newCidrNode, bit) - - insertedResult.appendAddedCidr(currentNode) - } else { - insertedResult.ResolutionAction = IGNORE_INSERTION - } - return []*InsertionResult{insertedResult} - - case SUBCIDR: - insertedResult.ConflictType = SUBCIDR - insertedResult.ConflictedWith = *currentNode - - if comparator(newCidrNode, currentNode) { - // we will take care of splitting later, (at the last bit) - // because we need to fill all bits for the newCidr - supernetToSplitLater = currentNode - insertedResult.ResolutionAction = SPLIT_EXISTING_CIDR - } else { - insertedResult.ResolutionAction = IGNORE_INSERTION - return []*InsertionResult{insertedResult} - } - - case SUPERCIDR: - insertedResult.ConflictType = SUPERCIDR - // since it is a super we do not know how many it will conflict with - insertedResults := []*InsertionResult{} - - currentNode.Metadata = newCidrNode.Metadata - conflictedCidrs := currentNode.GetLeafs() - - var anyConflictedCidrHasPriority bool - // we can not split on the spot, we need to clean - // the tree first (if any needed to be remove) - toSplitAround := []*trie.BinaryTrie[Metadata]{} - for _, conflictedCidr := range conflictedCidrs { - if comparator(conflictedCidr, newCidrNode) { - anyConflictedCidrHasPriority = true - toSplitAround = append(toSplitAround, conflictedCidr) - } else { - insertedResult = BuildNewResult(*ipnet, SUPERCIDR, REMOVE_EXISTING_CIDR, *conflictedCidr) - insertedResult.appendRemovedCidr(conflictedCidr) - insertedResults = append(insertedResults, insertedResult) - - // we remove it from the tree - conflictedCidr.DetachBranch(currentDepth + 1) - } - // since there is more than one result, we need to save this result - } - - // we split safely after cleaning the tree - for _, subcidr := range toSplitAround { - newCidrs := splitSuperAroundSub(currentNode, subcidr, newCidrNode.Metadata) - // populate the result - insertedResult = BuildNewResult(*ipnet, SUPERCIDR, SPLIT_INSERTED_CIDR, *subcidr) - insertedResult.ConflictedWith = *subcidr - insertedResult.ResolutionAction = SPLIT_INSERTED_CIDR - for _, splittedCidr := range newCidrs { - insertedResult.appendAddedCidr(splittedCidr) - } - insertedResults = append(insertedResults, insertedResult) - } - - if anyConflictedCidrHasPriority { - currentNode.Metadata = nil // Revert metadata change if new CIDR is not accepted. - // TODO: verify this later - //insertedResult.ResolutionAction = IGNORE_INSERTION - return insertedResults - } else { - // non of the conflicted CIDRS have win over this super - // so will of them are removed - currentNode = currentNode.Parent.AddChildOrReplaceAt(newCidrNode, bit) - insertedResult.appendAddedCidr(currentNode) - return insertedResults - } - - case NONE: - if currentDepth == depth { - currentNode = currentNode.Parent.AddChildOrReplaceAt(newCidrNode, bit) - insertedResult.AddedCIDRs = append(insertedResult.AddedCIDRs, *currentNode) - if currentNode != newCidrNode { - panic("New CIDR failed to be added at the expected location.") - } - if supernetToSplitLater != nil { - // since we has SUBCIDR conflict earlier, - // we do the splitting at the last bit here - insertedResult.appendRemovedCidr(supernetToSplitLater) - newCidrs := splitSuperAroundSub(supernetToSplitLater, currentNode, supernetToSplitLater.Metadata) - for _, splittedCidr := range newCidrs { - insertedResult.appendAddedCidr(splittedCidr) - } - } - } - // Continue traversal if no conflict and not the last bit. - } - } - - // no conflicted, so the result is normal - return []*InsertionResult{insertedResult} -} - -// isThereAConflict determines if there is a conflict between the current trie node and a new CIDR insertion attempt, -// categorizing the conflict type based on the targeted depth and the current node's characteristics. -// -// Parameters: -// - currentNode: current node in the trie. -// - targetedDepth: The depth in the trie at which the new CIDR is intended to be inserted. -// -// Returns: -// - A ConflictType value indicating the type of conflict, if any. -// -// The function evaluates the current node's metadata and its position in the trie relative to the targeted depth. -// It identifies if the current node represents a supernet, subnet, or equal CIDR conflict based on the insertion depth. -// -// Conflict Types: -// - SUPERCIDR: The current node is a supernet relative to the targeted CIDR. -// - SUBCIDR: The current node is a subnetwork relative to the targeted CIDR. -// - EQUAL_CIDR: The current node and the targeted CIDR are at the same depth. -// - NONE: There is no conflict. -func isThereAConflict(currentNode *trie.BinaryTrie[Metadata], targetedDepth int) ConflictType { - // Check if the current node is a new or path node without specific metadata. - if currentNode.Metadata == nil { - // Determine if the current node is a supernet of the targeted CIDR. - if targetedDepth < currentNode.GetDepth() && !currentNode.IsLeaf() { - return ConflictType(SUPERCIDR) // The node spans over the area of the new CIDR. - } else { - return ConflictType(NONE) // No conflict detected. - } - } else { - // Evaluate the relationship based on depths. - if currentNode.GetDepth()-1 == targetedDepth { - return ConflictType(EQUAL_CIDR) // The node is at the same level as the targeted CIDR. - } - if currentNode.GetDepth() <= targetedDepth { - return ConflictType(SUBCIDR) // The node is a subnetwork of the targeted CIDR. - } - } - - // If none of the conditions are met, there's an unhandled case. - panic("isThereAConflict: unhandled edge case encountered") -} - -// splitSuperAroundSub adjusts a super CIDR's trie structure around a specified sub CIDR by inserting sibling nodes. -// This process involves branching off at each step from the SUB-CIDR node upwards towards the SUPER-CIDR node, -// ensuring that the appropriate splits in the trie are made to represent the network structure correctly. -// -// Parameters: -// - super: super CIDR. -// - sub: sub CIDR to be surrounded. -// - splittedCidrMetadata: Metadata for the new CIDR nodes that will be created during the split. -// -// Returns: -// - A slice of pointers to nodes that were newly added during the splitting process. -// -// Panics: -// - If splittedCidrMetadata is nil, as metadata is essential for creating new trie nodes. -// -// The function traverses from the sub-CIDR node upwards, attempting to insert a sibling node at each step. -// If a sibling node at a given position does not exist, it is created and added. The traversal and modifications -// stop when reaching the depth of the super-CIDR node. -func splitSuperAroundSub(super *trie.BinaryTrie[Metadata], sub *trie.BinaryTrie[Metadata], splittedCidrMetadata *Metadata) []*trie.BinaryTrie[Metadata] { - if splittedCidrMetadata == nil { - panic("Metadata is required to split a supernet") - } - - var splittedCidrs []*trie.BinaryTrie[Metadata] - - sub.ForEachStepUp(func(current *trie.BinaryTrie[Metadata]) { - // Determine the opposite position to branch at (XOR with 1). - oppositePosition := current.GetPos() ^ 1 - parent := current.Parent - - // Create a new trie node with the same metadata as the splittedCidrMetadata. - newCidr := trie.NewTrieWithMetadata(&Metadata{ - Priority: splittedCidrMetadata.Priority, - Attributes: splittedCidrMetadata.Attributes, // Additional attributes from metadata. - }) - - // Try to add the new node as a sibling if it does not already exist. - added := parent.AddChildAtIfNotExist(newCidr, oppositePosition) - if added == newCidr { - // If the node was successfully added, append it to the list of split CIDRs. - splittedCidrs = append(splittedCidrs, added) - } else { - } - }, func(nextNode *trie.BinaryTrie[Metadata]) bool { - // Stop propagation when reaching the depth of the super-CIDR. - return nextNode.GetDepth() > super.GetDepth() - }) - - return splittedCidrs -} - -// LookupIP searches for the closest matching CIDR for a given IP address within the supernet. -// -// Parameters: -// - ip: A string representing the IP address -// -// Returns: -// - net.IPNet representing the closest matching CIDR, if found, or nil -// - An error if the IP address cannot be parsed -// -// The function parses the input IP address into a CIDR with a full netmask (32 for IPv4, 128 for IPv6). -// It then converts this CIDR into a slice of bits and traverses the corresponding trie (IPv4 or IPv6) -// to find the most specific matching CIDR. If the trie node representing the CIDR is a leaf or no further -// children exist for matching, the search concludes, returning the found CIDR or nil if no match exists. -func (super *Supernet) LookupIP(ip string) (*net.IPNet, error) { - // Determine if the IP is IPv4 or IPv6 based on the presence of a colon. - isV6 := strings.Contains(ip, ":") - mask := 32 - supernet := super.ipv4Cidrs // Default to IPv4 supernet. - - if isV6 { - mask = 128 - supernet = super.ipv6Cidrs // Use IPv6 supernet if the IP is IPv6. - } - - // Parse the IP address with a full netmask to form a valid CIDR for bit conversion. - _, parsedIP, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ip, mask)) - if err != nil { - return nil, err // Return parsing errors. - } - - ipBits, _ := cidrToBits(parsedIP) // Convert the parsed CIDR to a slice of bits. - - // Traverse the trie to find the most specific matching CIDR. - for i, bit := range ipBits { - if supernet == nil { - // Return nil if no matching CIDR is found in the trie. - return nil, nil - } else if supernet.IsLeaf() { - // Return the CIDR up to the current bit index if a leaf node is reached. - return bitsToCidr(ipBits[:i], isV6), nil - } else { - // Move to the next child node based on the current bit. - supernet = supernet.GetChildAt(bit) - } - } - - // The loop should always return before reaching this point. - panic("LookupIP: reached an unexpected state: the CIDR trie traversal should not get here.") -} - -// comparator evaluates two trie nodes, `a` and `b`, to determine if the new node `a` should replace the old node `b` -// based on their priority values. It is assumed that `a` is the new node and `b` is the old node. -// -// Parameters: -// - a: new CIDR entrya. -// - b: the old CIDR entry. -// -// Returns: -// - true if `a` should replace `b` or if they are considered equal in priority; false otherwise. -// -// Note: -// - The function assumes that if all priorities of `a` are equal to `b`, then `a` should be greater than `b`. -// - The priorities are compared in a lexicographical order, similar to comparing version numbers or tuples. -func comparator(a *trie.BinaryTrie[Metadata], b *trie.BinaryTrie[Metadata]) bool { - // Default to true, assuming 'a' is equal or greater unless proven otherwise. - result := true - - // Compare priority values lexicographically. - for i := range a.Metadata.Priority { - if a.Metadata.Priority[i] < b.Metadata.Priority[i] { - // If any priority of 'a' is less than 'b', return false immediately. - return false - } - } - // If all priorities of 'a' are greater than or equal to those of 'b', return true. - return result -} - -// bitsToCidr converts a slice of binary bits into a net.IPNet structure that represents a CIDR. -// This is used to form the IP address and subnet mask from a binary representation. -// -// Parameters: -// - bits: A slice of integers (0 or 1) representing the binary form of the IP address. -// - ipV6: A boolean flag indicating whether the address is IPv6 (true) or IPv4 (false). -// -// Returns: -// - A pointer to a net.IPNet structure that includes both the IP address and the subnet mask. -// -// This function dynamically constructs the IP and mask based on the length of the bits slice and the type of IP (IPv4 or IPv6). -// It supports a flexible number of bits and automatically adjusts for IPv4 (up to 32 bits) and IPv6 (up to 128 bits). -// -// Example: -// -// For a bits slice representing "192.168.1.1" and ipV6 set to false, the function would return an IPNet with the IP "192.168.1.1" -// and a full subnet mask "255.255.255.255" if all bits are provided. -func bitsToCidr(bits []int, ipV6 bool) *net.IPNet { - maxBytes := 4 - if ipV6 { - maxBytes = 16 // Set the byte limit to 16 for IPv6 - } - - ipBytes := make([]byte, 0, maxBytes) - maskBytes := make([]byte, 0, maxBytes) - currentBit := 0 - bitsLen := len(bits) - 1 - - for iByte := 0; iByte < maxBytes; iByte++ { - var ipByte byte - var maskByte byte - for i := 0; i < 8; i++ { - if currentBit <= bitsLen { - ipByte = ipByte<<1 | byte(bits[currentBit]) - maskByte = maskByte<<1 | 1 // Add a bit to the mask for each bit processed - currentBit++ - } else { - ipByte = ipByte << 1 // Shift the byte to the left, filling with zeros - maskByte = maskByte << 1 - } - } - ipBytes = append(ipBytes, ipByte) - maskBytes = append(maskBytes, maskByte) - } - - return &net.IPNet{ - IP: net.IP(ipBytes), - Mask: net.IPMask(maskBytes), - } -} - -// NodeToCidr converts a given trie node into a CIDR (Classless Inter-Domain Routing) string representation. -// This function uses the node's path to generate the CIDR string. -// -// Parameters: -// - t: Pointer to a trie.BinaryTrie node of type Metadata. It must contain valid metadata and a path. -// - isV6: A boolean indicating whether the IP version is IPv6. True means IPv6, false means IPv4. -// -// Returns: -// - A string representing the CIDR notation of the node's IP address. -// -// Panics: -// - If the node's metadata is nil, indicating that it is a path node without associated CIDR data, -// this function will panic with a specific error message. -// -// Example: -// -// Given a trie node representing an IP address with metadata, this function will output the address in CIDR format, -// like "192.168.1.0/24" for IPv4 or "2001:db8::/32" for IPv6. -func NodeToCidr(t *trie.BinaryTrie[Metadata]) string { - if t.Metadata == nil { - panic("Cannot convert a trie path node to CIDR: metadata is missing") - } - // Convert the binary path of the trie node to CIDR format using the bitsToCidr function, - // then convert the resulting net.IPNet object to a string. - return bitsToCidr(t.GetPath(), t.Metadata.IsV6).String() -} - -// cidrToBits converts a net.IPNet object into a slice of integers representing the binary bits of the network address. -// Additionally, it returns the depth of the network mask. -// -// The function panics if: -// - ipnet is nil, indicating invalid input. -// - the network mask is /0, which is technically valid but not supported by this library. -// -// Parameters: -// - ipnet: Pointer to a net.IPNet object containing the IP address and the network mask. -// -// Returns: -// -// - A slice of integers representing the binary format of the IP address up to the length of the network mask. -// -// - An integer representing the number of bits in the network mask minus one. -// -// Example: -// For IP address "192.168.1.1/24", this function would return a slice with the first 24 bits of the address in binary form, -// and the number 23 as the depth. -func cidrToBits(ipnet *net.IPNet) ([]int, int) { - if ipnet == nil { - panic("cidrToBits: IPNet is nil: validate the input before calling cidrToBits") - } - - maskSize, _ := ipnet.Mask.Size() - if maskSize == 0 { - panic("cidrToBits: network Mask /0 not valid: " + ipnet.String()) - } - - path := make([]int, maskSize) - currentBit := 0 - - // Process each byte of the IP address to convert it into bits. - for _, byteVal := range ipnet.IP { - // Iterate over each bit in the byte. - for bitPosition := 0; bitPosition < 8; bitPosition++ { - // Shift the byte to the right to place the bit at the most significant position (leftmost), - // and mask it with 1 to isolate the bit. - bit := (byteVal >> (7 - bitPosition)) & 1 - path[currentBit] = int(bit) - - // If we have processed bits equal to the size of the network mask, return the result. - if currentBit == (maskSize - 1) { - return path, maskSize - 1 - } - currentBit++ - } - } - - // This line should not be reached; if it is, there is an error in bit calculation. - panic("cidrToBits: bit calculation error - did not process enough bits for the mask size") -} From ceeb2ad4cd465555ca4f6cf55455d0485a71e7d9 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 20:58:10 +0300 Subject: [PATCH 02/12] refactor(Trie): rename methods --- pkg/supernet/conflict.go | 2 +- pkg/supernet/supernet.go | 6 ++-- pkg/supernet/supernet_test.go | 12 ++++---- pkg/supernet/uitls.go | 2 +- pkg/trie/trie.go | 55 +++++++++++++++-------------------- pkg/trie/trie_test.go | 36 +++++++++++------------ 6 files changed, 52 insertions(+), 61 deletions(-) diff --git a/pkg/supernet/conflict.go b/pkg/supernet/conflict.go index ef569d4..4428249 100644 --- a/pkg/supernet/conflict.go +++ b/pkg/supernet/conflict.go @@ -47,7 +47,7 @@ func (_ SuperCIDR) Resolve(conflictPoint *trie.BinaryTrie[Metadata], newSuperCid // since this is a super, we do not know how many subcidrs yet conflicting with this super // let us get all subCidrs - conflictedSubCidrs := conflictPoint.GetLeafs() + conflictedSubCidrs := conflictPoint.Leafs() subCidrsWithLowPriority := []*trie.BinaryTrie[Metadata]{} subCidrsWithHighPriority := []*trie.BinaryTrie[Metadata]{} diff --git a/pkg/supernet/supernet.go b/pkg/supernet/supernet.go index ca463c0..06ce2f4 100644 --- a/pkg/supernet/supernet.go +++ b/pkg/supernet/supernet.go @@ -140,7 +140,7 @@ func (super *Supernet) GetAllV4Cidrs(forV6 bool) []*trie.BinaryTrie[Metadata] { if forV6 { supernet = super.ipv6Cidrs } - return supernet.GetLeafs() + return supernet.Leafs() } // retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. @@ -156,8 +156,8 @@ func (super *Supernet) getAllV4CidrsString(forV6 bool) []string { supernet = super.ipv6Cidrs } var cidrs []string - for _, node := range supernet.GetLeafs() { - cidrs = append(cidrs, BitsToCidr(node.GetPath(), forV6).String()) + for _, node := range supernet.Leafs() { + cidrs = append(cidrs, BitsToCidr(node.Path(), forV6).String()) } return cidrs } diff --git a/pkg/supernet/supernet_test.go b/pkg/supernet/supernet_test.go index 1a94b3f..4f84ce5 100644 --- a/pkg/supernet/supernet_test.go +++ b/pkg/supernet/supernet_test.go @@ -99,7 +99,7 @@ func TestInsertAndRetrieveCidrs(t *testing.T) { assert.ElementsMatch(t, ipv4Results, super.getAllV4CidrsString(false), "IPv4 CIDR retrieval should match") ipv6ExpectedPath := []int{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} - assert.ElementsMatch(t, ipv6ExpectedPath, super.ipv6Cidrs.GetLeafsPaths()[0], "IPv6 path should match") + assert.ElementsMatch(t, ipv6ExpectedPath, super.ipv6Cidrs.LeafsPaths()[0], "IPv6 path should match") } func TestEqualConflictLowPriory(t *testing.T) { @@ -117,7 +117,7 @@ func TestEqualConflictLowPriory(t *testing.T) { "192.168.0.0/16", }, root.getAllV4CidrsString(false)) - assert.Equal(t, "high", root.ipv4Cidrs.GetLeafs()[0].Metadata().Attributes["cidr"]) + assert.Equal(t, "high", root.ipv4Cidrs.Leafs()[0].Metadata().Attributes["cidr"]) } func TestEqualConflictHighPriory(t *testing.T) { @@ -134,7 +134,7 @@ func TestEqualConflictHighPriory(t *testing.T) { "192.168.0.0/16", }, root.getAllV4CidrsString(false)) - assert.Equal(t, "high", root.ipv4Cidrs.GetLeafs()[0].Metadata().Attributes["cidr"]) + assert.Equal(t, "high", root.ipv4Cidrs.Leafs()[0].Metadata().Attributes["cidr"]) } @@ -562,11 +562,11 @@ func printResults(results *InsertionResult) { } func printPaths(root *Supernet) { - for _, node := range root.ipv4Cidrs.GetLeafs() { + for _, node := range root.ipv4Cidrs.Leafs() { if node.Metadata() != nil { - fmt.Printf("%v [%s] -> from [%s]\n", node.GetPath(), BitsToCidr(node.GetPath(), false).String(), node.Metadata().Attributes["cidr"]) + fmt.Printf("%v [%s] -> from [%s]\n", node.Path(), BitsToCidr(node.Path(), false).String(), node.Metadata().Attributes["cidr"]) } else { - fmt.Printf("%v <-!!-- [%s] \n", node.GetPath(), BitsToCidr(node.GetPath(), false).String()) + fmt.Printf("%v <-!!-- [%s] \n", node.Path(), BitsToCidr(node.Path(), false).String()) } } } diff --git a/pkg/supernet/uitls.go b/pkg/supernet/uitls.go index 1f3f04a..7ad26fb 100644 --- a/pkg/supernet/uitls.go +++ b/pkg/supernet/uitls.go @@ -81,7 +81,7 @@ func NodeToCidr(t *trie.BinaryTrie[Metadata]) string { } // Convert the binary path of the trie node to CIDR format using the bitsToCidr function, // then convert the resulting net.IPNet object to a string. - return BitsToCidr(t.GetPath(), t.Metadata().IsV6).String() + return BitsToCidr(t.Path(), t.Metadata().IsV6).String() } // CidrToBits converts a net.IPNet object into a slice of integers representing the binary bits of the network address. diff --git a/pkg/trie/trie.go b/pkg/trie/trie.go index b6cc9be..f76cdc8 100644 --- a/pkg/trie/trie.go +++ b/pkg/trie/trie.go @@ -2,25 +2,14 @@ package trie import "fmt" -type Trie[T any] interface { - Parent() *Trie[T] - Metadata() *T - Pos() int - Depth() int - Child(pos int) *Trie[T] - Sibling() *Trie[T] - AttachChild(child *Trie[T], pos int) *Trie[T] - AttachSibling(sibling *Trie[T]) - Detach() - DetachBranch() -} - // is an alias for int used to define child positions in a trie node. type ChildPos = int // Constants representing possible child positions in the trie. -const ZERO ChildPos = 0 -const ONE ChildPos = 1 +const ( + ZERO ChildPos = 0 + ONE ChildPos = 1 +) // BinaryTrie is a generic type representing a node in a trie. type BinaryTrie[T any] struct { @@ -31,7 +20,7 @@ type BinaryTrie[T any] struct { depth int // The depth of this node in the trie } -// NewTrieWithMetadata creates a new trie node with the provided metadata and initializes it. +// creates a new trie node with the provided metadata and initializes it. func NewTrieWithMetadata[T any](metadata *T) *BinaryTrie[T] { return &BinaryTrie[T]{ metadata: metadata, @@ -45,8 +34,8 @@ func NewTrie() *BinaryTrie[string] { return NewTrieWithMetadata(&s) } -// isRoot checks if the current node is the root of the trie. -func (t *BinaryTrie[T]) isRoot() bool { +// checks if the current node is the root of the trie. +func (t *BinaryTrie[T]) IsRoot() bool { return t.parent == nil } @@ -58,15 +47,17 @@ func (t *BinaryTrie[T]) Pos() int { return 0 } -// Depth returns the depth of the node in the trie. +// returns the depth of the node in the trie. func (t *BinaryTrie[T]) Depth() int { return t.depth } +// return the parent node, it will return nil if the node is root node func (t *BinaryTrie[T]) Parent() *BinaryTrie[T] { return t.parent } +// return the generic metadata func (t *BinaryTrie[T]) Metadata() *T { return t.metadata } @@ -124,7 +115,7 @@ func (t *BinaryTrie[T]) IsBranch() bool { // Detach will discount the node from the tree // if there is no reference to the node, it will be GC'ed func (t *BinaryTrie[T]) Detach() { - if !t.isRoot() { + if !t.IsRoot() { t.parent.children[t.Pos()] = nil } else { panic("[BUG] Detach: You can not Detach the root") @@ -143,12 +134,12 @@ func (t *BinaryTrie[T]) Detach() { // will return the branch node parent func (t *BinaryTrie[T]) DetachBranch(limit int) *BinaryTrie[T] { // if it has children - if t.isRoot() { + if t.IsRoot() { panic("[Bug] DetachBranch: You can not detach Root") } nearestBranchedNode := t t.ForEachStepUp(func(next *BinaryTrie[T]) { - if !next.parent.isRoot() { + if !next.parent.IsRoot() { nearestBranchedNode = next.parent } @@ -163,17 +154,17 @@ func (t *BinaryTrie[T]) DetachBranch(limit int) *BinaryTrie[T] { // checks if the node is a leaf (has no children). func (t *BinaryTrie[T]) IsLeaf() bool { - return t.children[0] == nil && t.children[1] == nil + return t.children[ZERO] == nil && t.children[ONE] == nil } // applies a function to each non-nil child of the node. // will return the original node t func (t *BinaryTrie[T]) ForEachChild(f func(t *BinaryTrie[T])) *BinaryTrie[T] { - if t.children[0] != nil { - f(t.children[0]) + if t.children[ZERO] != nil { + f(t.children[ZERO]) } - if t.children[1] != nil { - f(t.children[1]) + if t.children[ONE] != nil { + f(t.children[ONE]) } return t } @@ -212,7 +203,7 @@ func (t *BinaryTrie[T]) ForEachStepUp(f func(*BinaryTrie[T]), while func(*Binary // return the path from the root node // the path is an array of 0's and 1's // reverse it if you need the path form the child to the root -func (t *BinaryTrie[T]) GetPath() []int { +func (t *BinaryTrie[T]) Path() []int { path := []int{} t.ForEachStepUp(func(tr *BinaryTrie[T]) { @@ -224,7 +215,7 @@ func (t *BinaryTrie[T]) GetPath() []int { } // return all the leafs on the tree -func (t *BinaryTrie[T]) GetLeafs() []*BinaryTrie[T] { +func (t *BinaryTrie[T]) Leafs() []*BinaryTrie[T] { leafs := []*BinaryTrie[T]{} t.ForEachStepDown(func(t *BinaryTrie[T]) { if t.IsLeaf() { @@ -237,11 +228,11 @@ func (t *BinaryTrie[T]) GetLeafs() []*BinaryTrie[T] { // Generate an array of leafs paths which is uniq by definition // the path is from the root to leaf // reverse it if you need the path from leaf to root -func (root *BinaryTrie[T]) GetLeafsPaths() [][]int { +func (root *BinaryTrie[T]) LeafsPaths() [][]int { paths := [][]int{} root.ForEachStepDown(func(t *BinaryTrie[T]) { if t.IsLeaf() { - paths = append(paths, t.GetPath()) + paths = append(paths, t.Path()) } }, nil) return paths @@ -255,7 +246,7 @@ func (t *BinaryTrie[T]) String(printOnLeaf func(*BinaryTrie[T]) string) { extra = printOnLeaf(node) } - fmt.Printf("%v %s\n", node.GetPath(), extra) + fmt.Printf("%v %s\n", node.Path(), extra) } }, nil) } diff --git a/pkg/trie/trie_test.go b/pkg/trie/trie_test.go index 21386d6..1f8699b 100644 --- a/pkg/trie/trie_test.go +++ b/pkg/trie/trie_test.go @@ -96,7 +96,7 @@ func TestGetPath(t *testing.T) { child := root.AttachChild(NewTrie(), ONE) grandchild := child.AttachChild(NewTrie(), ZERO) - path := grandchild.GetPath() + path := grandchild.Path() expectedPath := []int{1, 0} assert.Equal(t, expectedPath, path, "Path should correctly represent the bits from root to grandchild") } @@ -113,7 +113,7 @@ func TestGetUniquePaths(t *testing.T) { {1, 0, 1, 0, 1, 0}, {1, 1, 1, 1}, } - actualPaths := root.GetLeafsPaths() + actualPaths := root.LeafsPaths() assert.ElementsMatch(t, expectedPaths, actualPaths, "Unique paths should match the expected paths") } @@ -123,7 +123,7 @@ func TestGetSibling(t *testing.T) { root := NewTrie() generateTrieAs(paths, root) - leafs := root.GetLeafs() + leafs := root.Leafs() assert.NotNil(t, leafs[0].Sibling()) assert.Equal(t, 1, leafs[0].Sibling().Pos()) assert.Nil(t, leafs[1].Sibling()) @@ -134,7 +134,7 @@ func TestAddSiblingIfNotExist(t *testing.T) { root := NewTrie() generateTrieAs(paths, root) - leafs := root.GetLeafs() + leafs := root.Leafs() assert.NotNil(t, leafs[0].Sibling()) assert.Nil(t, leafs[1].Sibling()) sibling := NewTrie() @@ -148,7 +148,7 @@ func TestAddSiblingIfExist(t *testing.T) { root := NewTrie() generateTrieAs(paths, root) - leafs := root.GetLeafs() + leafs := root.Leafs() assert.NotNil(t, leafs[0].Sibling()) sibling := NewTrie() leafs[0].AttachSibling(sibling) @@ -162,20 +162,20 @@ func TestDetach(t *testing.T) { assert.ElementsMatch(t, [][]int{ {0, 0, 1, 0}, {0, 0, 1, 1}, - }, root.GetLeafsPaths()) - leafs := root.GetLeafs() + }, root.LeafsPaths()) + leafs := root.Leafs() leafs[0].Detach() - newLeafs := root.GetLeafs() + newLeafs := root.Leafs() assert.Equal(t, 1, len(newLeafs)) - assert.Equal(t, []int{0, 0, 1, 1}, newLeafs[0].GetPath()) + assert.Equal(t, []int{0, 0, 1, 1}, newLeafs[0].Path()) leafs[1].Detach() - newLeafs = root.GetLeafs() + newLeafs = root.Leafs() assert.Equal(t, 1, len(newLeafs)) - assert.Equal(t, []int{0, 0, 1}, newLeafs[0].GetPath()) + assert.Equal(t, []int{0, 0, 1}, newLeafs[0].Path()) } func TestDetachBranch(t *testing.T) { @@ -191,9 +191,9 @@ func TestDetachBranch(t *testing.T) { expectedPaths := [][]int{ {0, 0, 1, 0}, } - lastLeaf := root.GetLeafs() + lastLeaf := root.Leafs() lastLeaf[1].DetachBranch(0) - actualPaths := root.GetLeafsPaths() + actualPaths := root.LeafsPaths() assert.ElementsMatch(t, expectedPaths, actualPaths, "Unique paths should match the expected paths") // case where the bench is the first @@ -207,9 +207,9 @@ func TestDetachBranch(t *testing.T) { expectedPaths = [][]int{ {0, 1}, } - lastLeaf = root.GetLeafs() + lastLeaf = root.Leafs() lastLeaf[1].DetachBranch(0) - actualPaths = root.GetLeafsPaths() + actualPaths = root.LeafsPaths() assert.ElementsMatch(t, expectedPaths, actualPaths, "Unique paths should match the expected paths") } @@ -250,7 +250,7 @@ func BenchmarkRead32BitPaths(b *testing.B) { } } - paths = root.GetLeafsPaths() + paths = root.LeafsPaths() maxPaths := len(paths) b.ResetTimer() @@ -260,7 +260,7 @@ func BenchmarkRead32BitPaths(b *testing.B) { randomPath := paths[rand.Intn(maxPaths)] for _, pos := range randomPath { if node == nil { - fmt.Printf("Node is nil \npr node path: %v\n random path is:%v\n", pr.GetPath(), randomPath) + fmt.Printf("Node is nil \npr node path: %v\n random path is:%v\n", pr.Path(), randomPath) panic("node is nil") } pr = node @@ -280,7 +280,7 @@ func BenchmarkGetLeafPaths(b *testing.B) { } // b.ResetTimer() this is to fast, so it redo the benchmark forever! - root.GetLeafsPaths() + root.LeafsPaths() } From 186f5e75f26a084201eb038f6e199f46fb68d65c Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 21:01:03 +0300 Subject: [PATCH 03/12] update(Trie): update docs --- pkg/trie/doc.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/trie/doc.go b/pkg/trie/doc.go index 54e88a8..0ac9d0f 100644 --- a/pkg/trie/doc.go +++ b/pkg/trie/doc.go @@ -10,16 +10,16 @@ // // Add child nodes // child1 := trie.NewTrieWithMetadata[string]("child1") // root := trie.NewTrieWithMetadata[string]("root") -// root.AddChildOrReplaceAt(child1, trie.ZERO) +// root.AddChild(child1, trie.ZERO) // // child2 := trie.NewTrieWithMetadata[string]("child2") -// root.AddChildOrReplaceAt(child2, trie.ONE) +// root.AddChild(child2, trie.ONE) // // // Check if a node is the root // fmt.Println("Is root:", root.isRoot()) // Output: Is root: true // // // Get the depth of a node -// fmt.Println("Depth of child1:", child1.GetDepth()) // Output: Depth of child1: 1 +// fmt.Println("Depth of child1:", child1.Depth()) // Output: Depth of child1: 1 // // // Check if a node is a leaf // fmt.Println("Is child1 a leaf:", child1.IsLeaf()) // Output: Is child1 a leaf: false From f0c932c52668789af08cf8e194f25b5d69327da2 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 21:31:54 +0300 Subject: [PATCH 04/12] split the code logic in files --- cmd/supernet/main.go | 4 +- pkg/supernet/action.go | 55 +++--- pkg/supernet/conflict.go | 46 ++++- pkg/supernet/results.go | 61 +++++++ pkg/supernet/supernet.go | 319 ++++++++-------------------------- pkg/supernet/supernet_test.go | 26 +-- 6 files changed, 222 insertions(+), 289 deletions(-) create mode 100644 pkg/supernet/results.go diff --git a/cmd/supernet/main.go b/cmd/supernet/main.go index c71aa77..1b09af5 100644 --- a/cmd/supernet/main.go +++ b/cmd/supernet/main.go @@ -56,7 +56,7 @@ func writeJsonResults(super *supernet.Supernet, directory string, cidrCol string defer file.Close() encoder := json.NewEncoder(file) - cidrs := super.GetAllV4Cidrs(false) + cidrs := super.AllCIDRS(false) fmt.Println("Starting to write resolved CIDRs...") if _, err = file.Write([]byte("[")); err != nil { @@ -95,7 +95,7 @@ func writeCsvResults(super *supernet.Supernet, directory string, cidrCol string) writer := csv.NewWriter(file) defer writer.Flush() - cidrs := super.GetAllV4Cidrs(false) + cidrs := super.AllCIDRS(false) fmt.Println("Starting to write resolved CIDRs...") diff --git a/pkg/supernet/action.go b/pkg/supernet/action.go index 7ef927b..f7a670c 100644 --- a/pkg/supernet/action.go +++ b/pkg/supernet/action.go @@ -1,8 +1,6 @@ package supernet import ( - "fmt" - "github.com/khalid_nowaf/supernet/pkg/trie" ) @@ -124,33 +122,44 @@ func (_ SplitExistingCIDR) String() string { return "Split Existing CIDR" } -type ActionResult struct { - Action Action - AddedCidrs []trie.BinaryTrie[Metadata] - RemoveCidrs []trie.BinaryTrie[Metadata] +// to keep track of all removed CIDRs from resolving a conflict. +func (ar *ActionResult) appendRemovedCidr(cidr *trie.BinaryTrie[Metadata]) { + ar.RemoveCidrs = append(ar.RemoveCidrs, *cidr) } -func (ar ActionResult) String() string { - addedCidrs := []string{} - removedCidrs := []string{} +// The function traverses from the sub-CIDR node upwards, attempting to insert a sibling node at each step. +// If a sibling node at a given position does not exist, it is created and added. The traversal and modifications +// stop when reaching the depth of the super-CIDR node. +func splitAround(sub *trie.BinaryTrie[Metadata], newCidrMetadata *Metadata, limitDepth int) []*trie.BinaryTrie[Metadata] { + splittedCidrMetadata := newCidrMetadata - for _, added := range ar.AddedCidrs { - addedCidrs = append(addedCidrs, NodeToCidr(&added)) + if splittedCidrMetadata == nil { + panic("[BUG] splitAround: Metadata is required to split a supernet") } - for _, removed := range ar.RemoveCidrs { - removedCidrs = append(removedCidrs, NodeToCidr(&removed)) - } + var splittedCidrs []*trie.BinaryTrie[Metadata] - return fmt.Sprintf("Action Taken: %s, Added CIDRs: %v, Removed CIDRs: %v", ar.Action, addedCidrs, removedCidrs) -} + sub.ForEachStepUp(func(current *trie.BinaryTrie[Metadata]) { -// to keep track of all the added CIDRs from resolving a conflict. -func (ar *ActionResult) appendAddedCidr(cidr *trie.BinaryTrie[Metadata]) { - ar.AddedCidrs = append(ar.AddedCidrs, *cidr) -} + // Create a new trie node with the same metadata as the splittedCidrMetadata. + newCidr := trie.NewTrieWithMetadata(&Metadata{ + IsV6: splittedCidrMetadata.IsV6, + originCIDR: splittedCidrMetadata.originCIDR, + Priority: splittedCidrMetadata.Priority, + Attributes: splittedCidrMetadata.Attributes, + }) -// to keep track of all removed CIDRs from resolving a conflict. -func (ar *ActionResult) appendRemovedCidr(cidr *trie.BinaryTrie[Metadata]) { - ar.RemoveCidrs = append(ar.RemoveCidrs, *cidr) + added := current.AttachSibling(newCidr) + + if added == newCidr { + // If the node was successfully added, append it to the list of split CIDRs. + splittedCidrs = append(splittedCidrs, added) + } else { + } + }, func(nextNode *trie.BinaryTrie[Metadata]) bool { + // Stop propagation when reaching the depth of the super-CIDR. + return nextNode.Depth() > limitDepth + }) + + return splittedCidrs } diff --git a/pkg/supernet/conflict.go b/pkg/supernet/conflict.go index 4428249..87adf62 100644 --- a/pkg/supernet/conflict.go +++ b/pkg/supernet/conflict.go @@ -93,7 +93,6 @@ func (_ SubCIDR) Resolve(existingSuperCidr *trie.BinaryTrie[Metadata], newSubCid if comparator(newSubCidr.Metadata(), existingSuperCidr.Metadata()) { // subcidr has higher priority - plan.AddAction(InsertNewCIDR{}, newSubCidr) plan.AddAction(SplitExistingCIDR{}, existingSuperCidr) plan.AddAction(RemoveExistingCIDR{}, existingSuperCidr) @@ -107,3 +106,48 @@ func (_ SubCIDR) Resolve(existingSuperCidr *trie.BinaryTrie[Metadata], newSubCid func (_ SubCIDR) String() string { return "Sub CIDR" } + +// CIDR conflict detection, it check the current node if it conflicts with other CIDRS +func isThereAConflict(currentNode *trie.BinaryTrie[Metadata], targetedDepth int) ConflictType { + // Check if the current node is a new or path node without specific metadata. + if currentNode.Metadata() == nil { + // Determine if the current node is a supernet of the targeted CIDR. + if targetedDepth == currentNode.Depth() && !currentNode.IsLeaf() { + return SuperCIDR{} // The node spans over the area of the new CIDR. + } else { + return NoConflict{} // No conflict detected. + } + } else { + // Evaluate the relationship based on depths. + if currentNode.Depth() == targetedDepth { + return EqualCIDR{} // The node is at the same level as the targeted CIDR. + } + if currentNode.Depth() < targetedDepth { + return SubCIDR{} // The node is a subnetwork of the targeted CIDR. + } + } + + // If none of the conditions are met, there's an unhandled case. + panic("[BUG] isThereAConflict: unhandled edge case encountered") +} + +// comparator evaluates two trie nodes, `a` and `b`, to determine if the new node `a` should replace the old node `b` +// based on their priority values. It is assumed that `a` is the new node and `b` is the old node. +// +// Note: +// - The function assumes that if all priorities of `a` are equal to `b`, then `a` should be greater than `b`. +// - The priorities are compared in a lexicographical order, similar to comparing version numbers or tuples. +func comparator(a *Metadata, b *Metadata) bool { + // Compare priority values lexicographically. + for i := range a.Priority { + if a.Priority[i] > b.Priority[i] { + + // If any priority of 'a' is less than 'b', return false immediately. + return true + } else if a.Priority[i] < b.Priority[i] { + return false + } + } + // they are equal, so a is greater + return true +} diff --git a/pkg/supernet/results.go b/pkg/supernet/results.go new file mode 100644 index 0000000..fa2f4fc --- /dev/null +++ b/pkg/supernet/results.go @@ -0,0 +1,61 @@ +package supernet + +import ( + "fmt" + "net" + + "github.com/khalid_nowaf/supernet/pkg/trie" +) + +// records the outcome of attempting to insert a CIDR for reporting +type InsertionResult struct { + CIDR *net.IPNet // CIDR was attempted to be inserted. + actions []*ActionResult // the result of each action is taken + ConflictedWith []trie.BinaryTrie[Metadata] // array of conflicting nodes + ConflictType // the type of the conflict +} + +func (ir *InsertionResult) String() string { + str := "" + + if _, ok := ir.ConflictType.(NoConflict); !ok { + str += fmt.Sprintf("Detect %s conflict |", ir.ConflictType) + str += fmt.Sprintf("New CIDR %s conflicted with [", ir.CIDR) + for _, conflictedCidr := range ir.ConflictedWith { + str += fmt.Sprintf("%s ", NodeToCidr(&conflictedCidr)) + } + str += "] | " + } + + for _, action := range ir.actions { + str += fmt.Sprintf("%s", action.String()) + } + + return str +} + +type ActionResult struct { + Action Action + AddedCidrs []trie.BinaryTrie[Metadata] + RemoveCidrs []trie.BinaryTrie[Metadata] +} + +func (ar ActionResult) String() string { + addedCidrs := []string{} + removedCidrs := []string{} + + for _, added := range ar.AddedCidrs { + addedCidrs = append(addedCidrs, NodeToCidr(&added)) + } + + for _, removed := range ar.RemoveCidrs { + removedCidrs = append(removedCidrs, NodeToCidr(&removed)) + } + + return fmt.Sprintf("Action Taken: %s, Added CIDRs: %v, Removed CIDRs: %v", ar.Action, addedCidrs, removedCidrs) +} + +// to keep track of all the added CIDRs from resolving a conflict. +func (ar *ActionResult) appendAddedCidr(cidr *trie.BinaryTrie[Metadata]) { + ar.AddedCidrs = append(ar.AddedCidrs, *cidr) +} diff --git a/pkg/supernet/supernet.go b/pkg/supernet/supernet.go index 06ce2f4..94d0df6 100644 --- a/pkg/supernet/supernet.go +++ b/pkg/supernet/supernet.go @@ -8,74 +8,7 @@ import ( "github.com/khalid_nowaf/supernet/pkg/trie" ) -func buildPath(root *trie.BinaryTrie[Metadata], path []int) (lastNode *trie.BinaryTrie[Metadata], conflict ConflictType, remainingPath []int) { - currentNode := root - for currentDepth, bit := range path { - // add a pathNode, if the current node is nil - currentNode = currentNode.AttachChild(newPathNode(), bit) - - conflictType := isThereAConflict(currentNode, len(path)) - - // if the there is a conflict, return the conflicting point node, and the remaining bits (path) - if _, noConflict := conflictType.(NoConflict); !noConflict { - return currentNode, conflictType, path[currentDepth+1:] - } - } - return currentNode, NoConflict{}, []int{} // empty -} - -func insertLeaf(root *trie.BinaryTrie[Metadata], path []int, newCidrNode *trie.BinaryTrie[Metadata]) *InsertionResult { - insertionResults := &InsertionResult{ - CIDR: newCidrNode.Metadata().originCIDR, - } - - // buildPath will tell us the strategy to resolve the conflict if there is - // any. - lastNode, conflictType, remainingPath := buildPath(root, path) - insertionResults.ConflictType = conflictType - - // based on the conflict we will get resolve - // and the resolver will return a resolution plan for each conflict - plan := conflictType.Resolve(lastNode, newCidrNode) - insertionResults.ConflictedWith = append(insertionResults.ConflictedWith, plan.Conflicts...) - - for _, step := range plan.Steps { - // each plan has an action has an excitor, and return an action result - result := step.Action.Execute(newCidrNode, lastNode, step.TargetNode, remainingPath) - insertionResults.actions = append(insertionResults.actions, result) - } - - return insertionResults -} - -// records the outcome of attempting to insert a CIDR for reporting -type InsertionResult struct { - CIDR *net.IPNet // CIDR was attempted to be inserted. - actions []*ActionResult // the result of each action is taken - ConflictedWith []trie.BinaryTrie[Metadata] // array of conflicting nodes - ConflictType // the type of the conflict -} - -func (ir *InsertionResult) String() string { - str := "" - - if _, ok := ir.ConflictType.(NoConflict); !ok { - str += fmt.Sprintf("Detect %s conflict |", ir.ConflictType) - str += fmt.Sprintf("New CIDR %s conflicted with [", ir.CIDR) - for _, conflictedCidr := range ir.ConflictedWith { - str += fmt.Sprintf("%s ", NodeToCidr(&conflictedCidr)) - } - str += "] | " - } - - for _, action := range ir.actions { - str += fmt.Sprintf("%s", action.String()) - } - - return str -} - -// holds the properties for a CIDR node within a trie, including IP version, priority, and additional attributes. +// holds the properties for a CIDR node type Metadata struct { originCIDR *net.IPNet // copy of the CIDR, to track it, if it get splitted later due to conflict resolution IsV6 bool // is it IPv6 CIDR @@ -95,14 +28,6 @@ func NewMetadata(ipnet *net.IPNet) *Metadata { } } -// creates a Metadata instance with default values. -// -// Returns: -// - A pointer to a Metadata instance initialized with default values. -// func NewDefaultMetadata() *Metadata { -// return &Metadata{} -// } - // Supernet represents a structure containing both IPv4 and IPv6 CIDRs, each stored in a separate trie. type Supernet struct { ipv4Cidrs *trie.BinaryTrie[Metadata] @@ -110,9 +35,6 @@ type Supernet struct { } // initializes a new supernet instance with separate tries for IPv4 and IPv6 CIDRs. -// -// Returns: -// - A pointer to a newly initialized supernet instance. func NewSupernet() *Supernet { return &Supernet{ ipv4Cidrs: &trie.BinaryTrie[Metadata]{}, @@ -120,58 +42,8 @@ func NewSupernet() *Supernet { } } -// newPathNode creates a new trie node intended for path utilization without any associated metadata. -// -// Returns: -// - A pointer to a newly created trie.BinaryTrie node with no metadata. -func newPathNode() *trie.BinaryTrie[Metadata] { - return &trie.BinaryTrie[Metadata]{} -} - -// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. -// -// Parameters: -// - forV6: A boolean flag if th CIDR is IPv6 -// -// Returns: -// - A slice of TrieNode, each representing a CIDR in the specified trie. -func (super *Supernet) GetAllV4Cidrs(forV6 bool) []*trie.BinaryTrie[Metadata] { - supernet := super.ipv4Cidrs - if forV6 { - supernet = super.ipv6Cidrs - } - return supernet.Leafs() -} - -// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. -// -// Parameters: -// - forV6: A boolean flag if th CIDR is IPv6 -// -// Returns: -// - A slice of strings, each representing a CIDR in the specified trie. -func (super *Supernet) getAllV4CidrsString(forV6 bool) []string { - supernet := super.ipv4Cidrs - if forV6 { - supernet = super.ipv6Cidrs - } - var cidrs []string - for _, node := range supernet.Leafs() { - cidrs = append(cidrs, BitsToCidr(node.Path(), forV6).String()) - } - return cidrs -} - // InsertCidr attempts to insert a new CIDR into the supernet, handling conflicts according to predefined priorities. // It traverses through the trie, adding new nodes as needed and resolving conflicts when they occur. -// -// Parameters: -// - ipnet: net.IPNet, the CIDR to be inserted. -// - metadata: Metadata associated with the CIDR, used for conflict resolution and node creation. -// -// This function navigates through each bit of the new CIDR's path, trying to add a new node if it doesn't already exist, -// and handles various types of conflicts (EQUAL_CIDR, SUBCIDR, SUPERCIDR) by comparing the priorities of the involved CIDRs. - func (super *Supernet) InsertCidr(ipnet *net.IPNet, metadata *Metadata) *InsertionResult { root := super.ipv4Cidrs @@ -202,117 +74,25 @@ func (super *Supernet) InsertCidr(ipnet *net.IPNet, metadata *Metadata) *Inserti return results } -// Conflict Types: -// - SUPERCIDR: The current node is a supernet relative to the targeted CIDR. -// - SUBCIDR: The current node is a subnetwork relative to the targeted CIDR. -// - EQUAL_CIDR: The current node and the targeted CIDR are at the same depth. -// - NONE: There is no conflict. -func isThereAConflict(currentNode *trie.BinaryTrie[Metadata], targetedDepth int) ConflictType { - // Check if the current node is a new or path node without specific metadata. - if currentNode.Metadata() == nil { - // Determine if the current node is a supernet of the targeted CIDR. - if targetedDepth == currentNode.Depth() && !currentNode.IsLeaf() { - return SuperCIDR{} // The node spans over the area of the new CIDR. - } else { - return NoConflict{} // No conflict detected. - } - } else { - // Evaluate the relationship based on depths. - if currentNode.Depth() == targetedDepth { - return EqualCIDR{} // The node is at the same level as the targeted CIDR. - } - if currentNode.Depth() < targetedDepth { - return SubCIDR{} // The node is a subnetwork of the targeted CIDR. - } - } - - // If none of the conditions are met, there's an unhandled case. - panic("[BUG] isThereAConflict: unhandled edge case encountered") -} - -// splitAround adjusts a super CIDR's trie structure around a specified sub CIDR by inserting sibling nodes. -// This process involves branching off at each step from the SUB-CIDR node upwards towards the SUPER-CIDR node, -// ensuring that the appropriate splits in the trie are made to represent the network structure correctly. -// -// Parameters: -// - super: super CIDR. -// - sub: sub CIDR to be surrounded. -// - splittedCidrMetadata: Metadata for the new CIDR nodes that will be created during the split. -// -// Returns: -// - A slice of pointers to nodes that were newly added during the splitting process. -// -// Panics: -// - If splittedCidrMetadata is nil, as metadata is essential for creating new trie nodes. -// -// The function traverses from the sub-CIDR node upwards, attempting to insert a sibling node at each step. -// If a sibling node at a given position does not exist, it is created and added. The traversal and modifications -// stop when reaching the depth of the super-CIDR node. -func splitAround(sub *trie.BinaryTrie[Metadata], newCidrMetadata *Metadata, limitDepth int) []*trie.BinaryTrie[Metadata] { - splittedCidrMetadata := newCidrMetadata - - if splittedCidrMetadata == nil { - panic("[BUG] splitAround: Metadata is required to split a supernet") - } - - var splittedCidrs []*trie.BinaryTrie[Metadata] - - sub.ForEachStepUp(func(current *trie.BinaryTrie[Metadata]) { - - // Create a new trie node with the same metadata as the splittedCidrMetadata. - newCidr := trie.NewTrieWithMetadata(&Metadata{ - IsV6: splittedCidrMetadata.IsV6, - originCIDR: splittedCidrMetadata.originCIDR, - Priority: splittedCidrMetadata.Priority, - Attributes: splittedCidrMetadata.Attributes, // Additional attributes from metadata. - }) - - added := current.AttachSibling(newCidr) - - if added == newCidr { - // If the node was successfully added, append it to the list of split CIDRs. - splittedCidrs = append(splittedCidrs, added) - } else { - } - }, func(nextNode *trie.BinaryTrie[Metadata]) bool { - // Stop propagation when reaching the depth of the super-CIDR. - return nextNode.Depth() > limitDepth - }) - - return splittedCidrs -} - // LookupIP searches for the closest matching CIDR for a given IP address within the supernet. -// -// Parameters: -// - ip: A string representing the IP address -// -// Returns: -// - net.IPNet representing the closest matching CIDR, if found, or nil -// - An error if the IP address cannot be parsed -// -// The function parses the input IP address into a CIDR with a full netmask (32 for IPv4, 128 for IPv6). -// It then converts this CIDR into a slice of bits and traverses the corresponding trie (IPv4 or IPv6) -// to find the most specific matching CIDR. If the trie node representing the CIDR is a leaf or no further -// children exist for matching, the search concludes, returning the found CIDR or nil if no match exists. func (super *Supernet) LookupIP(ip string) (*net.IPNet, error) { // Determine if the IP is IPv4 or IPv6 based on the presence of a colon. isV6 := strings.Contains(ip, ":") mask := 32 - supernet := super.ipv4Cidrs // Default to IPv4 supernet. + supernet := super.ipv4Cidrs if isV6 { mask = 128 - supernet = super.ipv6Cidrs // Use IPv6 supernet if the IP is IPv6. + supernet = super.ipv6Cidrs } // Parse the IP address with a full netmask to form a valid CIDR for bit conversion. _, parsedIP, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ip, mask)) if err != nil { - return nil, err // Return parsing errors. + return nil, err } - ipBits, _ := CidrToBits(parsedIP) // Convert the parsed CIDR to a slice of bits. + ipBits, _ := CidrToBits(parsedIP) // Traverse the trie to find the most specific matching CIDR. for i, bit := range ipBits { @@ -320,10 +100,8 @@ func (super *Supernet) LookupIP(ip string) (*net.IPNet, error) { // Return nil if no matching CIDR is found in the trie. return nil, nil } else if supernet.IsLeaf() { - // Return the CIDR up to the current bit index if a leaf node is reached. return BitsToCidr(ipBits[:i], isV6), nil } else { - // Move to the next child node based on the current bit. supernet = supernet.Child(bit) } } @@ -332,30 +110,71 @@ func (super *Supernet) LookupIP(ip string) (*net.IPNet, error) { panic("[BUG] LookupIP: reached an unexpected state, the CIDR trie traversal should not get here.") } -// comparator evaluates two trie nodes, `a` and `b`, to determine if the new node `a` should replace the old node `b` -// based on their priority values. It is assumed that `a` is the new node and `b` is the old node. -// -// Parameters: -// - a: new CIDR entrya. -// - b: the old CIDR entry. -// -// Returns: -// - true if `a` should replace `b` or if they are considered equal in priority; false otherwise. -// -// Note: -// - The function assumes that if all priorities of `a` are equal to `b`, then `a` should be greater than `b`. -// - The priorities are compared in a lexicographical order, similar to comparing version numbers or tuples. -func comparator(a *Metadata, b *Metadata) bool { - // Compare priority values lexicographically. - for i := range a.Priority { - if a.Priority[i] > b.Priority[i] { +// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. +func (super *Supernet) AllCIDRS(forV6 bool) []*trie.BinaryTrie[Metadata] { + supernet := super.ipv4Cidrs + if forV6 { + supernet = super.ipv6Cidrs + } + return supernet.Leafs() +} + +// retrieves all CIDRs from the specified IPv4 or IPv6 trie within a supernet. +func (super *Supernet) AllCidrsString(forV6 bool) []string { + supernet := super.ipv4Cidrs + if forV6 { + supernet = super.ipv6Cidrs + } + var cidrs []string + for _, node := range supernet.Leafs() { + cidrs = append(cidrs, BitsToCidr(node.Path(), forV6).String()) + } + return cidrs +} + +// creates a new trie node intended for path utilization without any associated metadata. +func newPathNode() *trie.BinaryTrie[Metadata] { + return &trie.BinaryTrie[Metadata]{} +} + +// build the CIDR path, and report any conflict +func buildPath(root *trie.BinaryTrie[Metadata], path []int) (lastNode *trie.BinaryTrie[Metadata], conflict ConflictType, remainingPath []int) { + currentNode := root + for currentDepth, bit := range path { + // add a pathNode, if the current node is nil + currentNode = currentNode.AttachChild(newPathNode(), bit) + + conflictType := isThereAConflict(currentNode, len(path)) - // If any priority of 'a' is less than 'b', return false immediately. - return true - } else if a.Priority[i] < b.Priority[i] { - return false + // if the there is a conflict, return the conflicting point node, and the remaining bits (path) + if _, noConflict := conflictType.(NoConflict); !noConflict { + return currentNode, conflictType, path[currentDepth+1:] } } - // they are equal, so a is greater - return true + return currentNode, NoConflict{}, []int{} // empty +} + +// try to build the CIDR path, and handle any conflict if any +func insertLeaf(root *trie.BinaryTrie[Metadata], path []int, newCidrNode *trie.BinaryTrie[Metadata]) *InsertionResult { + insertionResults := &InsertionResult{ + CIDR: newCidrNode.Metadata().originCIDR, + } + + // buildPath will tell us the strategy to resolve the conflict if there is + // any. + lastNode, conflictType, remainingPath := buildPath(root, path) + insertionResults.ConflictType = conflictType + + // based on the conflict we will get resolve + // and the resolver will return a resolution plan for each conflict + plan := conflictType.Resolve(lastNode, newCidrNode) + insertionResults.ConflictedWith = append(insertionResults.ConflictedWith, plan.Conflicts...) + + for _, step := range plan.Steps { + // each plan has an action has an excitor, and return an action result + result := step.Action.Execute(newCidrNode, lastNode, step.TargetNode, remainingPath) + insertionResults.actions = append(insertionResults.actions, result) + } + + return insertionResults } diff --git a/pkg/supernet/supernet_test.go b/pkg/supernet/supernet_test.go index 4f84ce5..8cf5373 100644 --- a/pkg/supernet/supernet_test.go +++ b/pkg/supernet/supernet_test.go @@ -96,7 +96,7 @@ func TestInsertAndRetrieveCidrs(t *testing.T) { } ipv4Results := []string{"1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8"} - assert.ElementsMatch(t, ipv4Results, super.getAllV4CidrsString(false), "IPv4 CIDR retrieval should match") + assert.ElementsMatch(t, ipv4Results, super.AllCidrsString(false), "IPv4 CIDR retrieval should match") ipv6ExpectedPath := []int{0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} assert.ElementsMatch(t, ipv6ExpectedPath, super.ipv6Cidrs.LeafsPaths()[0], "IPv6 path should match") @@ -115,7 +115,7 @@ func TestEqualConflictLowPriory(t *testing.T) { // subset assert.ElementsMatch(t, []string{ "192.168.0.0/16", - }, root.getAllV4CidrsString(false)) + }, root.AllCidrsString(false)) assert.Equal(t, "high", root.ipv4Cidrs.Leafs()[0].Metadata().Attributes["cidr"]) } @@ -132,7 +132,7 @@ func TestEqualConflictHighPriory(t *testing.T) { // subset assert.ElementsMatch(t, []string{ "192.168.0.0/16", - }, root.getAllV4CidrsString(false)) + }, root.AllCidrsString(false)) assert.Equal(t, "high", root.ipv4Cidrs.Leafs()[0].Metadata().Attributes["cidr"]) @@ -149,7 +149,7 @@ func TestSubConflictLowPriority(t *testing.T) { // subset assert.ElementsMatch(t, []string{ "192.168.0.0/16", - }, root.getAllV4CidrsString(false)) + }, root.AllCidrsString(false)) } func TestSubConflictHighPriority(t *testing.T) { @@ -161,7 +161,7 @@ func TestSubConflictHighPriority(t *testing.T) { results := root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) printPaths(root) printResults(results) - allCidrs := root.getAllV4CidrsString(false) + allCidrs := root.AllCidrsString(false) assert.Equal(t, len(allCidrs), 24-16+1) assert.ElementsMatch(t, []string{ @@ -174,7 +174,7 @@ func TestSubConflictHighPriority(t *testing.T) { "192.168.32.0/19", "192.168.64.0/18", "192.168.128.0/17", - }, root.getAllV4CidrsString(false)) + }, root.AllCidrsString(false)) } func TestSubConflictEqualPriority(t *testing.T) { root := NewSupernet() @@ -184,7 +184,7 @@ func TestSubConflictEqualPriority(t *testing.T) { root.InsertCidr(super, &Metadata{Priority: []uint8{0}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) - allCidrs := root.getAllV4CidrsString(false) + allCidrs := root.AllCidrsString(false) assert.Equal(t, 24-16+1, len(allCidrs)) @@ -208,7 +208,7 @@ func TestSuperConflictLowPriority(t *testing.T) { root.InsertCidr(sub, &Metadata{Priority: []uint8{1}, Attributes: makeCidrAtrr(sub.String())}) root.InsertCidr(super, &Metadata{Priority: []uint8{0}, Attributes: makeCidrAtrr(super.String())}) - allCidrs := root.getAllV4CidrsString(false) + allCidrs := root.AllCidrsString(false) assert.Equal(t, 24-16+1, len(allCidrs)) @@ -233,7 +233,7 @@ func TestSuperConflictHighPriority(t *testing.T) { root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) root.InsertCidr(super, &Metadata{Priority: []uint8{1}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) - allCidrs := root.getAllV4CidrsString(false) + allCidrs := root.AllCidrsString(false) assert.Equal(t, 1, len(allCidrs)) @@ -250,7 +250,7 @@ func TestSuperConflictEqualPriority(t *testing.T) { root.InsertCidr(sub, &Metadata{Priority: []uint8{0}, originCIDR: sub, Attributes: makeCidrAtrr(sub.String())}) result := root.InsertCidr(super, &Metadata{Priority: []uint8{0}, originCIDR: super, Attributes: makeCidrAtrr(super.String())}) printResults(result) - allCidrs := root.getAllV4CidrsString(false) + allCidrs := root.AllCidrsString(false) assert.Equal(t, 24-16+1, len(allCidrs)) @@ -474,7 +474,7 @@ func TestNestedConflictResolution1(t *testing.T) { "192.168.192.0/18", "192.168.128.0/19", "192.168.160.0/19", - }, root.getAllV4CidrsString(false)) + }, root.AllCidrsString(false)) } func TestNestedConflictResolution2(t *testing.T) { @@ -527,7 +527,7 @@ func TestNestedConflictResolution2(t *testing.T) { "192.168.64.0/18", "192.168.128.0/18", "192.168.192.0/18", - }, root.getAllV4CidrsString(false)) + }, root.AllCidrsString(false)) } func TestRemoveBranchWithLimit(t *testing.T) { @@ -548,7 +548,7 @@ func TestRemoveBranchWithLimit(t *testing.T) { printPaths(root) } - assert.ElementsMatch(t, []string{"192.168.128.0/18"}, root.getAllV4CidrsString(false)) + assert.ElementsMatch(t, []string{"192.168.128.0/18"}, root.AllCidrsString(false)) } func makeCidrAtrr(cidr string) map[string]string { From 3335acac23dcdefe7ae7f4222f54d5cf45edcc19 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 22:38:31 +0300 Subject: [PATCH 05/12] Feat(Options): allow to configure Supernet with options --- pkg/supernet/conflict.go | 59 +++++-------------------------- pkg/supernet/option.go | 21 +++++++++++ pkg/supernet/supernet.go | 65 ++++++++++++++++++++++++++++++----- pkg/supernet/supernet_test.go | 2 +- 4 files changed, 86 insertions(+), 61 deletions(-) create mode 100644 pkg/supernet/option.go diff --git a/pkg/supernet/conflict.go b/pkg/supernet/conflict.go index 87adf62..8a3622b 100644 --- a/pkg/supernet/conflict.go +++ b/pkg/supernet/conflict.go @@ -1,10 +1,12 @@ package supernet -import "github.com/khalid_nowaf/supernet/pkg/trie" +import ( + "github.com/khalid_nowaf/supernet/pkg/trie" +) type ConflictType interface { String() string - Resolve(conflictedCidr *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan + Resolve(conflictedCidr *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata], comparator func(a *Metadata, b *Metadata) bool) *ResolutionPlan } type ( @@ -14,7 +16,7 @@ type ( SubCIDR struct{} // the new CIDR is a sub CIDR of an existing super CIDR ) -func (_ NoConflict) Resolve(at *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { +func (_ NoConflict) Resolve(at *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata], comparator func(a *Metadata, b *Metadata) bool) *ResolutionPlan { plan := &ResolutionPlan{} plan.AddAction(InsertNewCIDR{}, at) return plan @@ -24,7 +26,7 @@ func (_ NoConflict) String() string { return "No Conflict" } -func (_ EqualCIDR) Resolve(conflictedCidr *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { +func (_ EqualCIDR) Resolve(conflictedCidr *trie.BinaryTrie[Metadata], newCidr *trie.BinaryTrie[Metadata], comparator func(a *Metadata, b *Metadata) bool) *ResolutionPlan { plan := &ResolutionPlan{} plan.Conflicts = append(plan.Conflicts, *conflictedCidr) if comparator(newCidr.Metadata(), conflictedCidr.Metadata()) { @@ -42,7 +44,7 @@ func (_ EqualCIDR) String() string { return "Equal CIDR" } -func (_ SuperCIDR) Resolve(conflictPoint *trie.BinaryTrie[Metadata], newSuperCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { +func (_ SuperCIDR) Resolve(conflictPoint *trie.BinaryTrie[Metadata], newSuperCidr *trie.BinaryTrie[Metadata], comparator func(a *Metadata, b *Metadata) bool) *ResolutionPlan { plan := &ResolutionPlan{} // since this is a super, we do not know how many subcidrs yet conflicting with this super @@ -84,7 +86,7 @@ func (_ SuperCIDR) String() string { return "Super CIDR" } -func (_ SubCIDR) Resolve(existingSuperCidr *trie.BinaryTrie[Metadata], newSubCidr *trie.BinaryTrie[Metadata]) *ResolutionPlan { +func (_ SubCIDR) Resolve(existingSuperCidr *trie.BinaryTrie[Metadata], newSubCidr *trie.BinaryTrie[Metadata], comparator func(a *Metadata, b *Metadata) bool) *ResolutionPlan { plan := &ResolutionPlan{} plan.Conflicts = append(plan.Conflicts, *existingSuperCidr) // since this is a SubCidr, we have 2 option @@ -106,48 +108,3 @@ func (_ SubCIDR) Resolve(existingSuperCidr *trie.BinaryTrie[Metadata], newSubCid func (_ SubCIDR) String() string { return "Sub CIDR" } - -// CIDR conflict detection, it check the current node if it conflicts with other CIDRS -func isThereAConflict(currentNode *trie.BinaryTrie[Metadata], targetedDepth int) ConflictType { - // Check if the current node is a new or path node without specific metadata. - if currentNode.Metadata() == nil { - // Determine if the current node is a supernet of the targeted CIDR. - if targetedDepth == currentNode.Depth() && !currentNode.IsLeaf() { - return SuperCIDR{} // The node spans over the area of the new CIDR. - } else { - return NoConflict{} // No conflict detected. - } - } else { - // Evaluate the relationship based on depths. - if currentNode.Depth() == targetedDepth { - return EqualCIDR{} // The node is at the same level as the targeted CIDR. - } - if currentNode.Depth() < targetedDepth { - return SubCIDR{} // The node is a subnetwork of the targeted CIDR. - } - } - - // If none of the conditions are met, there's an unhandled case. - panic("[BUG] isThereAConflict: unhandled edge case encountered") -} - -// comparator evaluates two trie nodes, `a` and `b`, to determine if the new node `a` should replace the old node `b` -// based on their priority values. It is assumed that `a` is the new node and `b` is the old node. -// -// Note: -// - The function assumes that if all priorities of `a` are equal to `b`, then `a` should be greater than `b`. -// - The priorities are compared in a lexicographical order, similar to comparing version numbers or tuples. -func comparator(a *Metadata, b *Metadata) bool { - // Compare priority values lexicographically. - for i := range a.Priority { - if a.Priority[i] > b.Priority[i] { - - // If any priority of 'a' is less than 'b', return false immediately. - return true - } else if a.Priority[i] < b.Priority[i] { - return false - } - } - // they are equal, so a is greater - return true -} diff --git a/pkg/supernet/option.go b/pkg/supernet/option.go new file mode 100644 index 0000000..46ad46e --- /dev/null +++ b/pkg/supernet/option.go @@ -0,0 +1,21 @@ +package supernet + +import "github.com/khalid_nowaf/supernet/pkg/trie" + +type Option func(*Supernet) *Supernet +type ComparatorOption func(a *Metadata, b *Metadata) bool + +func DefaultOptions() *Supernet { + return &Supernet{ + ipv4Cidrs: &trie.BinaryTrie[Metadata]{}, + ipv6Cidrs: &trie.BinaryTrie[Metadata]{}, + comparator: DefaultComparator, + } +} + +func WithComparator(comparator ComparatorOption) Option { + return func(s *Supernet) *Supernet { + s.comparator = comparator + return s + } +} diff --git a/pkg/supernet/supernet.go b/pkg/supernet/supernet.go index 94d0df6..966c205 100644 --- a/pkg/supernet/supernet.go +++ b/pkg/supernet/supernet.go @@ -30,16 +30,18 @@ func NewMetadata(ipnet *net.IPNet) *Metadata { // Supernet represents a structure containing both IPv4 and IPv6 CIDRs, each stored in a separate trie. type Supernet struct { - ipv4Cidrs *trie.BinaryTrie[Metadata] - ipv6Cidrs *trie.BinaryTrie[Metadata] + ipv4Cidrs *trie.BinaryTrie[Metadata] + ipv6Cidrs *trie.BinaryTrie[Metadata] + comparator ComparatorOption } // initializes a new supernet instance with separate tries for IPv4 and IPv6 CIDRs. -func NewSupernet() *Supernet { - return &Supernet{ - ipv4Cidrs: &trie.BinaryTrie[Metadata]{}, - ipv6Cidrs: &trie.BinaryTrie[Metadata]{}, +func NewSupernet(options ...Option) *Supernet { + super := DefaultOptions() + for _, option := range options { + super = option(super) } + return super } // InsertCidr attempts to insert a new CIDR into the supernet, handling conflicts according to predefined priorities. @@ -65,7 +67,7 @@ func (super *Supernet) InsertCidr(ipnet *net.IPNet, metadata *Metadata) *Inserti // add size of the subnet as priory copyMetadata.Priority = append(copyMetadata.Priority, uint8(depth)) copyMetadata.originCIDR = ipnet - results := insertLeaf( + results := super.insertLeaf( root, path, trie.NewTrieWithMetadata(copyMetadata), @@ -155,7 +157,7 @@ func buildPath(root *trie.BinaryTrie[Metadata], path []int) (lastNode *trie.Bina } // try to build the CIDR path, and handle any conflict if any -func insertLeaf(root *trie.BinaryTrie[Metadata], path []int, newCidrNode *trie.BinaryTrie[Metadata]) *InsertionResult { +func (super Supernet) insertLeaf(root *trie.BinaryTrie[Metadata], path []int, newCidrNode *trie.BinaryTrie[Metadata]) *InsertionResult { insertionResults := &InsertionResult{ CIDR: newCidrNode.Metadata().originCIDR, } @@ -167,7 +169,7 @@ func insertLeaf(root *trie.BinaryTrie[Metadata], path []int, newCidrNode *trie.B // based on the conflict we will get resolve // and the resolver will return a resolution plan for each conflict - plan := conflictType.Resolve(lastNode, newCidrNode) + plan := conflictType.Resolve(lastNode, newCidrNode, super.comparator) insertionResults.ConflictedWith = append(insertionResults.ConflictedWith, plan.Conflicts...) for _, step := range plan.Steps { @@ -178,3 +180,48 @@ func insertLeaf(root *trie.BinaryTrie[Metadata], path []int, newCidrNode *trie.B return insertionResults } + +// CIDR conflict detection, it check the current node if it conflicts with other CIDRS +func isThereAConflict(currentNode *trie.BinaryTrie[Metadata], targetedDepth int) ConflictType { + // Check if the current node is a new or path node without specific metadata. + if currentNode.Metadata() == nil { + // Determine if the current node is a supernet of the targeted CIDR. + if targetedDepth == currentNode.Depth() && !currentNode.IsLeaf() { + return SuperCIDR{} // The node spans over the area of the new CIDR. + } else { + return NoConflict{} // No conflict detected. + } + } else { + // Evaluate the relationship based on depths. + if currentNode.Depth() == targetedDepth { + return EqualCIDR{} // The node is at the same level as the targeted CIDR. + } + if currentNode.Depth() < targetedDepth { + return SubCIDR{} // The node is a subnetwork of the targeted CIDR. + } + } + + // If none of the conditions are met, there's an unhandled case. + panic("[BUG] isThereAConflict: unhandled edge case encountered") +} + +// evaluates two trie nodes, `a` and `b`, to determine if the new node `a` should replace the old node `b` +// based on their priority values. It is assumed that `a` is the new node and `b` is the old node. +// +// Note: +// - The function assumes that if all priorities of `a` are equal to `b`, then `a` should be greater than `b`. +// - The priorities are compared in a lexicographical order, similar to comparing version numbers or tuples. +func DefaultComparator(a *Metadata, b *Metadata) bool { + // Compare priority values lexicographically. + for i := range a.Priority { + if a.Priority[i] > b.Priority[i] { + + // If any priority of 'a' is less than 'b', return false immediately. + return true + } else if a.Priority[i] < b.Priority[i] { + return false + } + } + // they are equal, so a is greater + return true +} diff --git a/pkg/supernet/supernet_test.go b/pkg/supernet/supernet_test.go index 8cf5373..3a95b70 100644 --- a/pkg/supernet/supernet_test.go +++ b/pkg/supernet/supernet_test.go @@ -80,7 +80,7 @@ func TestTrieComparator(t *testing.T) { for _, comp := range comparisons { a.UpdateMetadata(&Metadata{Priority: comp.aPriority}) b.UpdateMetadata(&Metadata{Priority: comp.bPriority}) - assert.Equal(t, comp.expected, comparator(a.Metadata(), b.Metadata())) + assert.Equal(t, comp.expected, DefaultComparator(a.Metadata(), b.Metadata())) } } From 7dc105572bcdcf8fe442a888f09fd9b029d23944 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 22:42:28 +0300 Subject: [PATCH 06/12] refactor(CLI): move the CLI to it own package --- cmd/supernet/main.go | 126 +----------------------------------- cmd/supernet/parser.go | 141 ---------------------------------------- pkg/cli/cli.go | 129 +++++++++++++++++++++++++++++++++++++ pkg/cli/parser.go | 142 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 273 insertions(+), 265 deletions(-) create mode 100644 pkg/cli/cli.go create mode 100644 pkg/cli/parser.go diff --git a/cmd/supernet/main.go b/cmd/supernet/main.go index 1b09af5..ec3f7d7 100644 --- a/cmd/supernet/main.go +++ b/cmd/supernet/main.go @@ -1,136 +1,14 @@ -// supernet resolve file.[csv,json] resolved.csv --cidrCol cidr --priorityCol priority --priorityDel "|" package main import ( - "encoding/csv" - "encoding/json" "fmt" - "os" "github.com/alecthomas/kong" - "github.com/khalid_nowaf/supernet/pkg/supernet" + "github.com/khalid_nowaf/supernet/pkg/cli" ) -// ResolveCmd represents the command to resolve CIDR conflicts. -type ResolveCmd struct { - Files []string `arg:"" type:"existingfile" help:"Input file containing CIDRs in CSV or JSON format"` - CidrKey string `help:"Index of the CIDRs in the file" default:"cidr"` - PriorityKey string `help:"Index of the CIDRs priorities" default:"priority"` - PriorityDel string `help:"Delimiter for priorities in the field" default:" "` - Report bool `help:"Report only conflicted CIDRs"` -} - -// Run executes the resolve command. -func (cmd *ResolveCmd) Run(ctx *kong.Context) error { - fmt.Printf("%v \n", *cmd) - supernet := supernet.NewSupernet() - - for _, file := range cmd.Files { - if err := parseAndInsertCidrs(supernet, cmd, file); err != nil { - return err - } - if err := writeCsvResults(supernet, ".", cmd.CidrKey); err != nil { - - } - } - - return nil -} - -// parseAndInsertCidrs parses a file and inserts CIDRs into the supernet. -func parseAndInsertCidrs(super *supernet.Supernet, cmd *ResolveCmd, file string) error { - return parseCsv(cmd, file, func(cidr *CIDR) error { - result := super.InsertCidr(cidr.cidr, cidr.Metadata) - fmt.Println(result.String()) - return nil - }) -} - -// writeResults writes the results of CIDR resolution to a JSON file. -func writeJsonResults(super *supernet.Supernet, directory string, cidrCol string) error { - filePath := directory + "/resolved.json" - file, err := os.Create(filePath) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - cidrs := super.AllCIDRS(false) - - fmt.Println("Starting to write resolved CIDRs...") - if _, err = file.Write([]byte("[")); err != nil { - return err - } - - for i, cidr := range cidrs { - cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) - if i > 0 { - if _, err = file.Write([]byte(",")); err != nil { - return err - } - } - if err = encoder.Encode(cidr.Metadata().Attributes); err != nil { - return err - } - } - - if _, err = file.Write([]byte("]")); err != nil { - return err - } - fmt.Println("Writing complete.") - return nil -} - -// writeResults writes the results of CIDR resolution to a CSV file. -func writeCsvResults(super *supernet.Supernet, directory string, cidrCol string) error { - filePath := directory + "/resolved.csv" - file, err := os.Create(filePath) - if err != nil { - return err - } - defer file.Close() - - // Create a CSV writer - writer := csv.NewWriter(file) - defer writer.Flush() - - cidrs := super.AllCIDRS(false) - - fmt.Println("Starting to write resolved CIDRs...") - - // Optional: Write headers to the CSV file - headers := []string{} - for key := range cidrs[0].Metadata().Attributes { - headers = append(headers, key) - } - if err := writer.Write(headers); err != nil { - return err - } - - // Write data to the CSV file - for _, cidr := range cidrs { - cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) - record := make([]string, 0, len(cidr.Metadata().Attributes)) - // Ensure the fields are written in the same order as headers - for _, header := range headers { - record = append(record, cidr.Metadata().Attributes[header]) - } - if err := writer.Write(record); err != nil { - return err - } - } - - fmt.Println("Writing complete.") - return nil -} - -var CLI struct { - Resolve ResolveCmd `cmd:"" help:"Resolve CIDR conflicts"` -} - func main() { - ctx := kong.Parse(&CLI, kong.UsageOnError()) + ctx := kong.Parse(&cli.CLI, kong.UsageOnError()) if err := ctx.Run(); err != nil { fmt.Printf("Error: %v\n", err) } diff --git a/cmd/supernet/parser.go b/cmd/supernet/parser.go index 00289c0..06ab7d0 100644 --- a/cmd/supernet/parser.go +++ b/cmd/supernet/parser.go @@ -1,142 +1 @@ package main - -import ( - "encoding/csv" - "encoding/json" - "fmt" - "net" - "os" - "strconv" - "strings" - - "github.com/khalid_nowaf/supernet/pkg/supernet" -) - -type CIDR struct { - cidr *net.IPNet - *supernet.Metadata -} - -type Record map[string]string - -func parseJson(cmd *ResolveCmd, filepath string, onEachCidr func(cidr *CIDR) error) error { - file, err := os.Open(filepath) - if err != nil { - return err - } - defer file.Close() - - // Create a JSON Decoder - decoder := json.NewDecoder(file) - - // Read opening bracket of the array - _, err = decoder.Token() - if err != nil { - return err - } - - // Decode each element of the array - for decoder.More() { - data := Record{} - err := decoder.Decode(&data) - if err != nil { - return err - } - cidr, err := parseCIDR(data, cmd) - if err != nil { - return err - } - onEachCidr(cidr) - } - - // Read closing bracket of the array - _, err = decoder.Token() - if err != nil { - return err - } - - return nil -} - -func parseCsv(cmd *ResolveCmd, filepath string, onEachCidr func(cidr *CIDR) error) error { - file, err := os.Open(filepath) - if err != nil { - return err - } - defer file.Close() - - // Create a CSV Reader - reader := csv.NewReader(file) - - // Optionally, configure reader fields if necessary (e.g., reader.Comma = ';') - // reader.Comma = ' ' // default delimiter - // reader.Comment = '#' // example to ignore lines starting with '#' - - // Read the header to build the key mapping (assuming first line is the header) - headers, err := reader.Read() - if err != nil { - return err - } - - // Read each record from the CSV - for { - recordData, err := reader.Read() - if err != nil { - break // End of file or an error - } - - record := make(Record) - for i, value := range recordData { - record[headers[i]] = value - } - - cidr, err := parseCIDR(record, cmd) - if err != nil { - return err - } - err = onEachCidr(cidr) - if err != nil { - return err - } - } - - return nil -} - -func parseCIDR(record Record, cmd *ResolveCmd) (*CIDR, error) { - isV6 := false - - var priorities []uint8 - - _, cidr, err := net.ParseCIDR(record[cmd.CidrKey]) - if err != nil { - fmt.Printf("Key: %s CIDR: %s \nRecord: %v", cmd.CidrKey, record[cmd.CidrKey], record) - return nil, err - } - priorityIndex, founded := record[cmd.PriorityKey] - if founded { - prioritiesStr := strings.Split(priorityIndex, cmd.PriorityDel) - for _, priority := range prioritiesStr { - i, err := strconv.Atoi(priority) - - if err != nil { - return nil, fmt.Errorf("can not convert priority to Int for record: %v", record) - } - priorities = append(priorities, uint8(i)) - } - } else { - panic("No priorities values founded, use 0 as default " + cmd.PriorityKey) - priorities = []uint8{0} - } - - if cidr.IP.To4() == nil { - isV6 = true - } - return &CIDR{ - cidr: cidr, - Metadata: &supernet.Metadata{ - IsV6: isV6, - Priority: priorities, - Attributes: record, - }}, nil -} diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go new file mode 100644 index 0000000..33bc115 --- /dev/null +++ b/pkg/cli/cli.go @@ -0,0 +1,129 @@ +package cli + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "os" + + "github.com/alecthomas/kong" + "github.com/khalid_nowaf/supernet/pkg/supernet" +) + +// ResolveCmd represents the command to resolve CIDR conflicts. +type ResolveCmd struct { + Files []string `arg:"" type:"existingfile" help:"Input file containing CIDRs in CSV or JSON format"` + CidrKey string `help:"Index of the CIDRs in the file" default:"cidr"` + PriorityKey string `help:"Index of the CIDRs priorities" default:"priority"` + PriorityDel string `help:"Delimiter for priorities in the field" default:" "` + Report bool `help:"Report only conflicted CIDRs"` +} + +// Run executes the resolve command. +func (cmd *ResolveCmd) Run(ctx *kong.Context) error { + fmt.Printf("%v \n", *cmd) + supernet := supernet.NewSupernet() + + for _, file := range cmd.Files { + if err := parseAndInsertCidrs(supernet, cmd, file); err != nil { + return err + } + if err := writeCsvResults(supernet, ".", cmd.CidrKey); err != nil { + + } + } + + return nil +} + +// parseAndInsertCidrs parses a file and inserts CIDRs into the supernet. +func parseAndInsertCidrs(super *supernet.Supernet, cmd *ResolveCmd, file string) error { + return parseCsv(cmd, file, func(cidr *CIDR) error { + result := super.InsertCidr(cidr.cidr, cidr.Metadata) + fmt.Println(result.String()) + return nil + }) +} + +// writeResults writes the results of CIDR resolution to a JSON file. +func writeJsonResults(super *supernet.Supernet, directory string, cidrCol string) error { + filePath := directory + "/resolved.json" + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + cidrs := super.AllCIDRS(false) + + fmt.Println("Starting to write resolved CIDRs...") + if _, err = file.Write([]byte("[")); err != nil { + return err + } + + for i, cidr := range cidrs { + cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) + if i > 0 { + if _, err = file.Write([]byte(",")); err != nil { + return err + } + } + if err = encoder.Encode(cidr.Metadata().Attributes); err != nil { + return err + } + } + + if _, err = file.Write([]byte("]")); err != nil { + return err + } + fmt.Println("Writing complete.") + return nil +} + +// writeResults writes the results of CIDR resolution to a CSV file. +func writeCsvResults(super *supernet.Supernet, directory string, cidrCol string) error { + filePath := directory + "/resolved.csv" + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + // Create a CSV writer + writer := csv.NewWriter(file) + defer writer.Flush() + + cidrs := super.AllCIDRS(false) + + fmt.Println("Starting to write resolved CIDRs...") + + // Optional: Write headers to the CSV file + headers := []string{} + for key := range cidrs[0].Metadata().Attributes { + headers = append(headers, key) + } + if err := writer.Write(headers); err != nil { + return err + } + + // Write data to the CSV file + for _, cidr := range cidrs { + cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) + record := make([]string, 0, len(cidr.Metadata().Attributes)) + // Ensure the fields are written in the same order as headers + for _, header := range headers { + record = append(record, cidr.Metadata().Attributes[header]) + } + if err := writer.Write(record); err != nil { + return err + } + } + + fmt.Println("Writing complete.") + return nil +} + +var CLI struct { + Resolve ResolveCmd `cmd:"" help:"Resolve CIDR conflicts"` +} diff --git a/pkg/cli/parser.go b/pkg/cli/parser.go new file mode 100644 index 0000000..a4505aa --- /dev/null +++ b/pkg/cli/parser.go @@ -0,0 +1,142 @@ +package cli + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "net" + "os" + "strconv" + "strings" + + "github.com/khalid_nowaf/supernet/pkg/supernet" +) + +type CIDR struct { + cidr *net.IPNet + *supernet.Metadata +} + +type Record map[string]string + +func parseJson(cmd *ResolveCmd, filepath string, onEachCidr func(cidr *CIDR) error) error { + file, err := os.Open(filepath) + if err != nil { + return err + } + defer file.Close() + + // Create a JSON Decoder + decoder := json.NewDecoder(file) + + // Read opening bracket of the array + _, err = decoder.Token() + if err != nil { + return err + } + + // Decode each element of the array + for decoder.More() { + data := Record{} + err := decoder.Decode(&data) + if err != nil { + return err + } + cidr, err := parseCIDR(data, cmd) + if err != nil { + return err + } + onEachCidr(cidr) + } + + // Read closing bracket of the array + _, err = decoder.Token() + if err != nil { + return err + } + + return nil +} + +func parseCsv(cmd *ResolveCmd, filepath string, onEachCidr func(cidr *CIDR) error) error { + file, err := os.Open(filepath) + if err != nil { + return err + } + defer file.Close() + + // Create a CSV Reader + reader := csv.NewReader(file) + + // Optionally, configure reader fields if necessary (e.g., reader.Comma = ';') + // reader.Comma = ' ' // default delimiter + // reader.Comment = '#' // example to ignore lines starting with '#' + + // Read the header to build the key mapping (assuming first line is the header) + headers, err := reader.Read() + if err != nil { + return err + } + + // Read each record from the CSV + for { + recordData, err := reader.Read() + if err != nil { + break // End of file or an error + } + + record := make(Record) + for i, value := range recordData { + record[headers[i]] = value + } + + cidr, err := parseCIDR(record, cmd) + if err != nil { + return err + } + err = onEachCidr(cidr) + if err != nil { + return err + } + } + + return nil +} + +func parseCIDR(record Record, cmd *ResolveCmd) (*CIDR, error) { + isV6 := false + + var priorities []uint8 + + _, cidr, err := net.ParseCIDR(record[cmd.CidrKey]) + if err != nil { + fmt.Printf("Key: %s CIDR: %s \nRecord: %v", cmd.CidrKey, record[cmd.CidrKey], record) + return nil, err + } + priorityIndex, founded := record[cmd.PriorityKey] + if founded { + prioritiesStr := strings.Split(priorityIndex, cmd.PriorityDel) + for _, priority := range prioritiesStr { + i, err := strconv.Atoi(priority) + + if err != nil { + return nil, fmt.Errorf("can not convert priority to Int for record: %v", record) + } + priorities = append(priorities, uint8(i)) + } + } else { + panic("No priorities values founded, use 0 as default " + cmd.PriorityKey) + priorities = []uint8{0} + } + + if cidr.IP.To4() == nil { + isV6 = true + } + return &CIDR{ + cidr: cidr, + Metadata: &supernet.Metadata{ + IsV6: isV6, + Priority: priorities, + Attributes: record, + }}, nil +} From ca4e082ff4586b62b021008b83b5e2bb22514ab6 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 23:08:19 +0300 Subject: [PATCH 07/12] feat(CLI): inject supernet to the CLI --- cmd/supernet/main.go | 9 +-- cmd/supernet/parser.go | 1 - pkg/cli/cli.go | 121 +++------------------------------------- pkg/cli/reslove.go | 122 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 121 deletions(-) delete mode 100644 cmd/supernet/parser.go create mode 100644 pkg/cli/reslove.go diff --git a/cmd/supernet/main.go b/cmd/supernet/main.go index ec3f7d7..c3f214a 100644 --- a/cmd/supernet/main.go +++ b/cmd/supernet/main.go @@ -1,15 +1,10 @@ package main import ( - "fmt" - - "github.com/alecthomas/kong" "github.com/khalid_nowaf/supernet/pkg/cli" + "github.com/khalid_nowaf/supernet/pkg/supernet" ) func main() { - ctx := kong.Parse(&cli.CLI, kong.UsageOnError()) - if err := ctx.Run(); err != nil { - fmt.Printf("Error: %v\n", err) - } + cli.NewCLI(supernet.NewSupernet()) } diff --git a/cmd/supernet/parser.go b/cmd/supernet/parser.go deleted file mode 100644 index 06ab7d0..0000000 --- a/cmd/supernet/parser.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 33bc115..b511e5e 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -1,129 +1,24 @@ package cli import ( - "encoding/csv" - "encoding/json" "fmt" - "os" "github.com/alecthomas/kong" "github.com/khalid_nowaf/supernet/pkg/supernet" ) // ResolveCmd represents the command to resolve CIDR conflicts. -type ResolveCmd struct { - Files []string `arg:"" type:"existingfile" help:"Input file containing CIDRs in CSV or JSON format"` - CidrKey string `help:"Index of the CIDRs in the file" default:"cidr"` - PriorityKey string `help:"Index of the CIDRs priorities" default:"priority"` - PriorityDel string `help:"Delimiter for priorities in the field" default:" "` - Report bool `help:"Report only conflicted CIDRs"` +type Context struct { + super *supernet.Supernet } -// Run executes the resolve command. -func (cmd *ResolveCmd) Run(ctx *kong.Context) error { - fmt.Printf("%v \n", *cmd) - supernet := supernet.NewSupernet() - - for _, file := range cmd.Files { - if err := parseAndInsertCidrs(supernet, cmd, file); err != nil { - return err - } - if err := writeCsvResults(supernet, ".", cmd.CidrKey); err != nil { - - } - } - - return nil -} - -// parseAndInsertCidrs parses a file and inserts CIDRs into the supernet. -func parseAndInsertCidrs(super *supernet.Supernet, cmd *ResolveCmd, file string) error { - return parseCsv(cmd, file, func(cidr *CIDR) error { - result := super.InsertCidr(cidr.cidr, cidr.Metadata) - fmt.Println(result.String()) - return nil - }) -} - -// writeResults writes the results of CIDR resolution to a JSON file. -func writeJsonResults(super *supernet.Supernet, directory string, cidrCol string) error { - filePath := directory + "/resolved.json" - file, err := os.Create(filePath) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - cidrs := super.AllCIDRS(false) - - fmt.Println("Starting to write resolved CIDRs...") - if _, err = file.Write([]byte("[")); err != nil { - return err - } - - for i, cidr := range cidrs { - cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) - if i > 0 { - if _, err = file.Write([]byte(",")); err != nil { - return err - } - } - if err = encoder.Encode(cidr.Metadata().Attributes); err != nil { - return err - } - } - - if _, err = file.Write([]byte("]")); err != nil { - return err - } - fmt.Println("Writing complete.") - return nil +var cli struct { + Resolve ResolveCmd `cmd:"" help:"Resolve CIDR conflicts"` } -// writeResults writes the results of CIDR resolution to a CSV file. -func writeCsvResults(super *supernet.Supernet, directory string, cidrCol string) error { - filePath := directory + "/resolved.csv" - file, err := os.Create(filePath) - if err != nil { - return err - } - defer file.Close() - - // Create a CSV writer - writer := csv.NewWriter(file) - defer writer.Flush() - - cidrs := super.AllCIDRS(false) - - fmt.Println("Starting to write resolved CIDRs...") - - // Optional: Write headers to the CSV file - headers := []string{} - for key := range cidrs[0].Metadata().Attributes { - headers = append(headers, key) +func NewCLI(super *supernet.Supernet) { + ctx := kong.Parse(cli, kong.UsageOnError()) + if err := ctx.Run(&Context{super: super}); err != nil { + fmt.Printf("Error: %v\n", err) } - if err := writer.Write(headers); err != nil { - return err - } - - // Write data to the CSV file - for _, cidr := range cidrs { - cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) - record := make([]string, 0, len(cidr.Metadata().Attributes)) - // Ensure the fields are written in the same order as headers - for _, header := range headers { - record = append(record, cidr.Metadata().Attributes[header]) - } - if err := writer.Write(record); err != nil { - return err - } - } - - fmt.Println("Writing complete.") - return nil -} - -var CLI struct { - Resolve ResolveCmd `cmd:"" help:"Resolve CIDR conflicts"` } diff --git a/pkg/cli/reslove.go b/pkg/cli/reslove.go new file mode 100644 index 0000000..aaf6a21 --- /dev/null +++ b/pkg/cli/reslove.go @@ -0,0 +1,122 @@ +package cli + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "os" + + "github.com/khalid_nowaf/supernet/pkg/supernet" +) + +type ResolveCmd struct { + Files []string `arg:"" type:"existingfile" help:"Input file containing CIDRs in CSV or JSON format"` + CidrKey string `help:"Index of the CIDRs in the file" default:"cidr"` + PriorityKey string `help:"Index of the CIDRs priorities" default:"priority"` + PriorityDel string `help:"Delimiter for priorities in the field" default:" "` + Report bool `help:"Report only conflicted CIDRs"` +} + +// Run executes the resolve command. +func (cmd *ResolveCmd) Run(ctx *Context) error { + fmt.Printf("%v \n", *cmd) + + for _, file := range cmd.Files { + if err := parseAndInsertCidrs(ctx.super, cmd, file); err != nil { + return err + } + if err := writeCsvResults(ctx.super, ".", cmd.CidrKey); err != nil { + + } + } + + return nil +} + +// parseAndInsertCidrs parses a file and inserts CIDRs into the supernet. +func parseAndInsertCidrs(super *supernet.Supernet, cmd *ResolveCmd, file string) error { + return parseCsv(cmd, file, func(cidr *CIDR) error { + result := super.InsertCidr(cidr.cidr, cidr.Metadata) + fmt.Println(result.String()) + return nil + }) +} + +// writeResults writes the results of CIDR resolution to a JSON file. +func writeJsonResults(super *supernet.Supernet, directory string, cidrCol string) error { + filePath := directory + "/resolved.json" + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + cidrs := super.AllCIDRS(false) + + fmt.Println("Starting to write resolved CIDRs...") + if _, err = file.Write([]byte("[")); err != nil { + return err + } + + for i, cidr := range cidrs { + cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) + if i > 0 { + if _, err = file.Write([]byte(",")); err != nil { + return err + } + } + if err = encoder.Encode(cidr.Metadata().Attributes); err != nil { + return err + } + } + + if _, err = file.Write([]byte("]")); err != nil { + return err + } + fmt.Println("Writing complete.") + return nil +} + +// writeResults writes the results of CIDR resolution to a CSV file. +func writeCsvResults(super *supernet.Supernet, directory string, cidrCol string) error { + filePath := directory + "/resolved.csv" + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + // Create a CSV writer + writer := csv.NewWriter(file) + defer writer.Flush() + + cidrs := super.AllCIDRS(false) + + fmt.Println("Starting to write resolved CIDRs...") + + // Optional: Write headers to the CSV file + headers := []string{} + for key := range cidrs[0].Metadata().Attributes { + headers = append(headers, key) + } + if err := writer.Write(headers); err != nil { + return err + } + + // Write data to the CSV file + for _, cidr := range cidrs { + cidr.Metadata().Attributes[cidrCol] = supernet.NodeToCidr(cidr) + record := make([]string, 0, len(cidr.Metadata().Attributes)) + // Ensure the fields are written in the same order as headers + for _, header := range headers { + record = append(record, cidr.Metadata().Attributes[header]) + } + if err := writer.Write(record); err != nil { + return err + } + } + + fmt.Println("Writing complete.") + return nil +} From 77bf28f25303ea53fb46d7369f689a4ae45a8354 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 23:11:34 +0300 Subject: [PATCH 08/12] fix(CLI): struct pointer --- pkg/cli/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index b511e5e..7e874df 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -17,7 +17,7 @@ var cli struct { } func NewCLI(super *supernet.Supernet) { - ctx := kong.Parse(cli, kong.UsageOnError()) + ctx := kong.Parse(&cli, kong.UsageOnError()) if err := ctx.Run(&Context{super: super}); err != nil { fmt.Printf("Error: %v\n", err) } From 17becc9c7c0517e77f49b6ec9aeffe7378127363 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 23:30:44 +0300 Subject: [PATCH 09/12] Feat(Supernet): Optional Loggers --- cmd/supernet/main.go | 2 +- pkg/cli/reslove.go | 5 +---- pkg/supernet/option.go | 21 ++++++++++++++++++++- pkg/supernet/supernet.go | 3 ++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/cmd/supernet/main.go b/cmd/supernet/main.go index c3f214a..d8a86a5 100644 --- a/cmd/supernet/main.go +++ b/cmd/supernet/main.go @@ -6,5 +6,5 @@ import ( ) func main() { - cli.NewCLI(supernet.NewSupernet()) + cli.NewCLI(supernet.NewSupernet(supernet.WithSimpleLogger())) } diff --git a/pkg/cli/reslove.go b/pkg/cli/reslove.go index aaf6a21..dea029f 100644 --- a/pkg/cli/reslove.go +++ b/pkg/cli/reslove.go @@ -19,8 +19,6 @@ type ResolveCmd struct { // Run executes the resolve command. func (cmd *ResolveCmd) Run(ctx *Context) error { - fmt.Printf("%v \n", *cmd) - for _, file := range cmd.Files { if err := parseAndInsertCidrs(ctx.super, cmd, file); err != nil { return err @@ -36,8 +34,7 @@ func (cmd *ResolveCmd) Run(ctx *Context) error { // parseAndInsertCidrs parses a file and inserts CIDRs into the supernet. func parseAndInsertCidrs(super *supernet.Supernet, cmd *ResolveCmd, file string) error { return parseCsv(cmd, file, func(cidr *CIDR) error { - result := super.InsertCidr(cidr.cidr, cidr.Metadata) - fmt.Println(result.String()) + super.InsertCidr(cidr.cidr, cidr.Metadata) return nil }) } diff --git a/pkg/supernet/option.go b/pkg/supernet/option.go index 46ad46e..b4ed21b 100644 --- a/pkg/supernet/option.go +++ b/pkg/supernet/option.go @@ -1,15 +1,21 @@ package supernet -import "github.com/khalid_nowaf/supernet/pkg/trie" +import ( + "fmt" + + "github.com/khalid_nowaf/supernet/pkg/trie" +) type Option func(*Supernet) *Supernet type ComparatorOption func(a *Metadata, b *Metadata) bool +type LoggerOption func(*InsertionResult) func DefaultOptions() *Supernet { return &Supernet{ ipv4Cidrs: &trie.BinaryTrie[Metadata]{}, ipv6Cidrs: &trie.BinaryTrie[Metadata]{}, comparator: DefaultComparator, + logger: func(ir *InsertionResult) {}, } } @@ -19,3 +25,16 @@ func WithComparator(comparator ComparatorOption) Option { return s } } + +func WithCustomLogger(logger LoggerOption) Option { + return func(s *Supernet) *Supernet { + s.logger = logger + return s + } +} + +func WithSimpleLogger() Option { + return WithCustomLogger(func(ir *InsertionResult) { + fmt.Println(ir.String()) + }) +} diff --git a/pkg/supernet/supernet.go b/pkg/supernet/supernet.go index 966c205..005bb26 100644 --- a/pkg/supernet/supernet.go +++ b/pkg/supernet/supernet.go @@ -33,6 +33,7 @@ type Supernet struct { ipv4Cidrs *trie.BinaryTrie[Metadata] ipv6Cidrs *trie.BinaryTrie[Metadata] comparator ComparatorOption + logger LoggerOption } // initializes a new supernet instance with separate tries for IPv4 and IPv6 CIDRs. @@ -72,7 +73,7 @@ func (super *Supernet) InsertCidr(ipnet *net.IPNet, metadata *Metadata) *Inserti path, trie.NewTrieWithMetadata(copyMetadata), ) - + super.logger(results) return results } From 765033dab9206462c7cae6f54405134379528108 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 23:33:07 +0300 Subject: [PATCH 10/12] update backlog --- backlog.todo | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backlog.todo b/backlog.todo index 8fcea61..bcccb6b 100644 --- a/backlog.todo +++ b/backlog.todo @@ -3,15 +3,16 @@ ☐ @Feat(Trie): Optimize feature to convert Trie to LCTrie @idea ☐ @Refactor(Supernet): extract binary operations to separate package @low ☐ @Refactor(Supernet): extract IPNet wrapping logic to file or a package @low + ☐ @Docs(Supernet): rewrite the comments ✔ @Feat(Supernet): Build cmd `supernet` that take a file and resolve the conflict @high @done(24-06-25 20:32) ✔ @Feat(Supernet): add a report function to report what is conflicted and how it was resolved @high @done(24-06-25 20:32) ✔ @Feat(Supernet): add recursive conflict resolution during splitting @high @high @done(24-06-25 20:32) ✔ replace AddChildIfNotExist with super.Insert and refactor the code if needed @done(24-06-25 20:32) ✔ find a way if to detect if the conflicted was created during the insertion, to avoid conflict resolution @done(24-06-25 20:32) - ☐ @Refactor(Supernet): clean the code - ☐ @Docs(Supernet): rewrite the comments - ☐ @Refactor(Supernet): make the resolvers and actions easy to track - ☐ @feat(Supernet): make the supernet configurable - ☐ @feat(Supernet): add custom comparator + ✔ @Refactor(Supernet): clean the code @done(24-06-30 23:32) + ✔ @Refactor(Supernet): make the resolvers and actions easy to track @done(24-06-30 23:32) + ✔ @feat(Supernet): make the supernet configurable @done(24-06-30 23:32) + ✔ @feat(Supernet): add custom comparator @done(24-06-30 23:32) + ✔ feat(Supernet): add custom logger @done(24-06-30 23:32) From e640d5e00232cb213bd1f47bc16a84b49d4f7c40 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 23:34:11 +0300 Subject: [PATCH 11/12] remove unwanted file --- desgin.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 desgin.md diff --git a/desgin.md b/desgin.md deleted file mode 100644 index 08affb3..0000000 --- a/desgin.md +++ /dev/null @@ -1,9 +0,0 @@ -## Conflict Reporter -- it should says the new cidr is conflicting with what? (Conflicted With) -- it should says what type of the conflict is happing (Conflict Type) -- it should says what is the actions is taken (ResolutionAction) -- it should says what happened (Added and Removed CIDRS) - - -### Edge case: If the inserted CIDR is a supernet of N subnet: -- each one conflict should have a conflict Report From 6745e81db9e27909a0915db044f1d0daac9ae9d1 Mon Sep 17 00:00:00 2001 From: Khalid Nowaf Date: Sun, 30 Jun 2024 23:56:32 +0300 Subject: [PATCH 12/12] feat(CLI): Logging flag --- backlog.todo | 3 +++ cmd/supernet/main.go | 2 +- pkg/cli/cli.go | 4 ++++ pkg/cli/parser.go | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/backlog.todo b/backlog.todo index bcccb6b..09f131e 100644 --- a/backlog.todo +++ b/backlog.todo @@ -4,6 +4,9 @@ ☐ @Refactor(Supernet): extract binary operations to separate package @low ☐ @Refactor(Supernet): extract IPNet wrapping logic to file or a package @low ☐ @Docs(Supernet): rewrite the comments + ☐ @Feat(CLI): detect file type from extension + ☐ @Feat(CLI): configurable output format + ✔ @Feat(CLI): logs flag @done(24-06-30 23:55) ✔ @Feat(Supernet): Build cmd `supernet` that take a file and resolve the conflict @high @done(24-06-25 20:32) ✔ @Feat(Supernet): add a report function to report what is conflicted and how it was resolved @high @done(24-06-25 20:32) ✔ @Feat(Supernet): add recursive conflict resolution during splitting @high @high @done(24-06-25 20:32) diff --git a/cmd/supernet/main.go b/cmd/supernet/main.go index d8a86a5..c3f214a 100644 --- a/cmd/supernet/main.go +++ b/cmd/supernet/main.go @@ -6,5 +6,5 @@ import ( ) func main() { - cli.NewCLI(supernet.NewSupernet(supernet.WithSimpleLogger())) + cli.NewCLI(supernet.NewSupernet()) } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 7e874df..f43cd56 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -13,11 +13,15 @@ type Context struct { } var cli struct { + Log bool `help:"Print the details about the inserted CIDR and the conflicts if any"` Resolve ResolveCmd `cmd:"" help:"Resolve CIDR conflicts"` } func NewCLI(super *supernet.Supernet) { ctx := kong.Parse(&cli, kong.UsageOnError()) + if cli.Log { + super = supernet.WithSimpleLogger()(super) + } if err := ctx.Run(&Context{super: super}); err != nil { fmt.Printf("Error: %v\n", err) } diff --git a/pkg/cli/parser.go b/pkg/cli/parser.go index a4505aa..e89de63 100644 --- a/pkg/cli/parser.go +++ b/pkg/cli/parser.go @@ -125,6 +125,7 @@ func parseCIDR(record Record, cmd *ResolveCmd) (*CIDR, error) { priorities = append(priorities, uint8(i)) } } else { + // TODO: check if the priority at same length, if not (mm maybe we fill the result with Zeros) panic("No priorities values founded, use 0 as default " + cmd.PriorityKey) priorities = []uint8{0} }