diff --git a/ipinfo/cmd_calc.go b/ipinfo/cmd_calc.go new file mode 100644 index 00000000..3b46c844 --- /dev/null +++ b/ipinfo/cmd_calc.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "github.com/ipinfo/cli/lib" + "github.com/ipinfo/cli/lib/complete" + "github.com/ipinfo/cli/lib/complete/predict" + "github.com/spf13/pflag" +) + +var completionsCalc = &complete.Command{ + Flags: map[string]complete.Predictor{ + "-h": predict.Nothing, + "--help": predict.Nothing, + }, +} + +// printHelpCalc prints the help message for the "calc" command. +func printHelpCalc() { + fmt.Printf( + `Usage: %s calc [] + +Description: + Evaluate a mathematical expression and print the result. + +Examples: + %[1]s calc "2*2828-1" + %[1]s calc "190.87.89.1*2" + %[1]s calc "2001:0db8:85a3:0000:0000:8a2e:0370:7334*6" + +Options: + General: + --help, -h + show help. +`, progBase) +} + +// cmdCalc is the handler for the "calc" command. +func cmdCalc() error { + f := lib.CmdCalcFlags{} + f.Init() + pflag.Parse() + + return lib.CmdCalc(f, pflag.Args()[1:], printHelpCalc) +} diff --git a/ipinfo/cmd_default.go b/ipinfo/cmd_default.go index 9926dc50..6db4e7fe 100644 --- a/ipinfo/cmd_default.go +++ b/ipinfo/cmd_default.go @@ -32,6 +32,7 @@ Commands: randip Generates random IPs. splitcidr splits a larger CIDR into smaller CIDRs. mmdb read, import and export mmdb files. + calc evaluates a mathematical expression that may contain IP addresses. tool misc. tools related to IPs, IP ranges and CIDRs. download download free ipinfo database files. cache manage the cache. diff --git a/ipinfo/cmd_tool.go b/ipinfo/cmd_tool.go index dfd7cedd..3dc8dc4b 100644 --- a/ipinfo/cmd_tool.go +++ b/ipinfo/cmd_tool.go @@ -12,6 +12,9 @@ import ( var completionsTool = &complete.Command{ Sub: map[string]*complete.Command{ "aggregate": completionsToolAggregate, + "ip2n": completionsToolIP2n, + "n2ip": completionsToolN2IP, + "n2ip6": completionsToolN2IP6, }, Flags: map[string]complete.Predictor{ "-h": predict.Nothing, @@ -26,6 +29,9 @@ func printHelpTool() { Commands: aggregate aggregate IPs, IP ranges, and CIDRs. + ip2n converts an IPv4 or IPv6 address to its decimal representation. + n2ip evaluates a mathematical expression and converts it to an IPv4 or IPv6. + n2ip6 evaluates a mathematical expression and converts it to an IPv6. Options: --help, -h @@ -56,6 +62,12 @@ func cmdTool() error { switch { case cmd == "aggregate": err = cmdToolAggregate() + case cmd == "ip2n": + err = cmdToolIP2n() + case cmd == "n2ip": + err = cmdToolN2IP() + case cmd == "n2ip6": + err = cmdToolN2IP6() default: err = toolHelp() } diff --git a/ipinfo/cmd_tool_ip2n.go b/ipinfo/cmd_tool_ip2n.go new file mode 100644 index 00000000..eb2a57e8 --- /dev/null +++ b/ipinfo/cmd_tool_ip2n.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "github.com/ipinfo/cli/lib" + "github.com/ipinfo/cli/lib/complete" + "github.com/ipinfo/cli/lib/complete/predict" + "github.com/spf13/pflag" +) + +var completionsToolIP2n = &complete.Command{ + Flags: map[string]complete.Predictor{ + "-h": predict.Nothing, + "--help": predict.Nothing, + }, +} + +// printHelpToolIp2n prints the help message for the "ip2n" command. +func printHelpToolIp2n() { + fmt.Printf( + `Usage: %s tool ip2n + +Description: + Converts an IPv4 or IPv6 address to its decimal representation. + +Examples: + %[1]s ip2n "190.87.89.1" + %[1]s ip2n "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + %[1]s ip2n "2001:0db8:85a3::8a2e:0370:7334" + %[1]s ip2n "::7334" + %[1]s ip2n "7334::" + +Options: + General: + --help, -h + show help. +`, progBase) +} + +// cmdToolIP2n is the handler for the "ip2n" command. +func cmdToolIP2n() error { + f := lib.CmdToolIP2nFlags{} + f.Init() + pflag.Parse() + + return lib.CmdToolIP2n(f, pflag.Args()[2:], printHelpToolIp2n) +} diff --git a/ipinfo/cmd_tool_n2ip.go b/ipinfo/cmd_tool_n2ip.go new file mode 100644 index 00000000..a1e96e01 --- /dev/null +++ b/ipinfo/cmd_tool_n2ip.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "github.com/ipinfo/cli/lib" + "github.com/ipinfo/cli/lib/complete" + "github.com/ipinfo/cli/lib/complete/predict" + "github.com/spf13/pflag" +) + +// cmdToolN2IP is the handler for the "n2ip" command. +var completionsToolN2IP = &complete.Command{ + Flags: map[string]complete.Predictor{ + "-h": predict.Nothing, + "--help": predict.Nothing, + "-6": predict.Set(predictReadFmts), + "--ipv6": predict.Set(predictReadFmts), + }, +} + +// printHelpToolN2IP prints the help message for the "n2ip" command. +func printHelpToolN2IP() { + fmt.Printf( + `Usage: %s n2ip tool [] + +Description: + Converts a given numeric representation to its corresponding IPv4 or IPv6 address, + and can also evaluate a mathematical expression for conversion. + +Examples: + %[1]s n2ip "4294967295 + 87" + %[1]s n2ip "4294967295" --ipv6 + %[1]s n2ip -6 "201523715" + %[1]s n2ip "51922968585348276285304963292200960" + %[1]s n2ip "a:: - 4294967295" + +Options: + General: + --help, -h + show help. + --ipv6, -6 + force conversion to IPv6 address +`, progBase) +} + +// cmdToolN2IP is the handler for the "n2ip" command. +func cmdToolN2IP() error { + f := lib.CmdToolN2IPFlags{} + f.Init() + pflag.Parse() + + return lib.CmdToolN2IP(f, pflag.Args()[2:], printHelpToolN2IP) +} diff --git a/ipinfo/cmd_tool_n2ip6.go b/ipinfo/cmd_tool_n2ip6.go new file mode 100644 index 00000000..2cc7dbdd --- /dev/null +++ b/ipinfo/cmd_tool_n2ip6.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "github.com/ipinfo/cli/lib" + "github.com/ipinfo/cli/lib/complete" + "github.com/ipinfo/cli/lib/complete/predict" + "github.com/spf13/pflag" +) + +var completionsToolN2IP6 = &complete.Command{ + Flags: map[string]complete.Predictor{ + "-h": predict.Nothing, + "--help": predict.Nothing, + }, +} + +// printHelpToolN2IP6 prints the help message for the "n2ip6" command. +func printHelpToolN2IP6() { + fmt.Printf( + `Usage: %s tool n2ip6 [] + +Description: + Converts a given numeric representation to its corresponding IPv6 address, + and can also evaluate a mathematical expression for conversion. + +Examples: + %[1]s n2ip6 "4294967295 + 87" + %[1]s n2ip6 "4294967295" + %[1]s n2ip6 "201523715" + %[1]s n2ip6 "51922968585348276285304963292200960" + %[1]s n2ip6 "a:: - 4294967295" + +Options: + General: + --help, -h + show help. +`, progBase) +} + +// cmdToolN2IP6 is the handler for the "n2ip6" command. +func cmdToolN2IP6() error { + f := lib.CmdToolN2IP6Flags{} + f.Init() + pflag.Parse() + + return lib.CmdToolN2IP6(f, pflag.Args()[2:], printHelpToolN2IP6) +} diff --git a/ipinfo/completions.go b/ipinfo/completions.go index 0287d836..b1d06a06 100644 --- a/ipinfo/completions.go +++ b/ipinfo/completions.go @@ -23,6 +23,7 @@ var completions = &complete.Command{ "randip": completionsRandIP, "splitcidr": completionsSplitCIDR, "mmdb": completionsMmdb, + "calc": completionsCalc, "tool": completionsTool, "download": completionsDownload, "cache": completionsCache, diff --git a/ipinfo/main.go b/ipinfo/main.go index c8426d59..9bb766c6 100644 --- a/ipinfo/main.go +++ b/ipinfo/main.go @@ -67,6 +67,8 @@ func main() { err = cmdSplitCIDR() case cmd == "mmdb": err = cmdMmdb() + case cmd == "calc": + err = cmdCalc() case cmd == "download": err = cmdDownload() case cmd == "tool": diff --git a/lib/cmd_calc.go b/lib/cmd_calc.go new file mode 100644 index 00000000..99f0432d --- /dev/null +++ b/lib/cmd_calc.go @@ -0,0 +1,345 @@ +package lib + +import ( + "fmt" + "github.com/fatih/color" + "github.com/spf13/pflag" + "math" + "math/big" + "net" + "regexp" + "strconv" + "strings" +) + +// CmdCalcFlags are flags expected by CmdCalc +type CmdCalcFlags struct { + Help bool + NoColor bool +} + +// Init initializes the common flags available to CmdCalc with sensible +func (f *CmdCalcFlags) Init() { + _h := "see description in --help" + pflag.BoolVarP( + &f.Help, + "help", "h", false, + "show help.", + ) + pflag.BoolVar( + &f.NoColor, + "nocolor", false, + _h, + ) +} + +// prec Function to return precedence of operators +func prec(s string) int { + if s == "^" { + return 3 + } else if (s == "/") || (s == "*") { + return 2 + } else if (s == "+") || (s == "-") { + return 1 + } else { + return -1 + } +} + +// isFloat Function to check if string is a float +func isFloat(str string) bool { + pattern := `^[-+]?\d+(\.\d+)?$` + + // Compile the regular expression pattern. + regex := regexp.MustCompile(pattern) + return regex.MatchString(str) +} + +// InfixToPostfix Function to convert infix expression to postfix expression using a stack based algorithm +func InfixToPostfix(infix []string) []string { + var postfixStack Stack + var postfix []string + + for _, token := range infix { + if isOperator(token) { + for { + topOfStack, isEmpty := postfixStack.Pop() + if isEmpty || prec(token) > prec(topOfStack) { + postfixStack.Push(topOfStack) + break + } + postfix = append(postfix, topOfStack) + } + postfixStack.Push(token) + } else if token == "(" { + postfixStack.Push(token) + } else if token == ")" { + for { + topOfStack, _ := postfixStack.Pop() + if topOfStack == "(" { + break + } + postfix = append(postfix, topOfStack) + } + } else { + postfix = append(postfix, token) + } + } + + // Pop all the remaining elements from the stack + topOfStack, isEmpty := postfixStack.Pop() + for !isEmpty { + postfix = append(postfix, topOfStack) + topOfStack, isEmpty = postfixStack.Pop() + } + return postfix +} + +// EvaluatePostfix Function to evaluate postfix expression using a stack based algorithm +func EvaluatePostfix(postfix []string) (*big.Float, error) { + // Precision for parsing string to big.Float + var precision uint = 10000 + var postfixStack Stack + for _, el := range postfix { + // if operand, push it onto the stack. + if el == "" { + continue + } + if isFloat(el) || StrIsIPv4Str(el) || StrIsIPv6Str(el) { + postfixStack.Push(el) + continue + } + + // if operator pop two elements off of the stack. + strNum1, isEmpty := postfixStack.Pop() + if isEmpty { + return big.NewFloat(0), ErrInvalidInput + } + num1, _, _ := big.ParseFloat(strNum1, 10, precision, big.ToZero) + + strNum2, isEmpty := postfixStack.Pop() + if isEmpty { + return big.NewFloat(0), ErrInvalidInput + } + num2, _, _ := big.ParseFloat(strNum2, 10, precision, big.ToZero) + + operator := el + result := new(big.Float) + + switch { + case operator == "+": + result = result.Add(num2, num1) + case operator == "-": + result = result.Sub(num2, num1) + case operator == "*": + result = result.Mul(num2, num1) + case operator == "/": + // Check for division by zero + if num1.Cmp(big.NewFloat(0)) == 0 { + return big.NewFloat(0), ErrInvalidInput + } + result = new(big.Float).Quo(num2, num1) + case operator == "^": + // Using Float64() to convert big.Float to float64 + // because big.Float does not have a equivalent function + // for math.Pow() which accepts big.Float + num1F64, _ := num1.Float64() + num2F64, _ := num2.Float64() + res := math.Pow(num2F64, num1F64) + result = new(big.Float).SetPrec(precision).SetFloat64(res) + default: + return big.NewFloat(0), ErrInvalidInput + } + strResult := result.Text('f', 50) + postfixStack.Push(strResult) + } + + strTop, _ := postfixStack.Pop() + top, _, _ := big.ParseFloat(strTop, 10, precision, big.ToZero) + + return top, nil +} + +// isOperator Function to check if token is an operator +func isOperator(token string) bool { + operators := map[string]bool{"+": true, "-": true, "*": true, "/": true, "^": true} + _, isOperator := operators[token] + return isOperator +} + +// translateToken Function to translate token to decimal i.e. convert ipv4, ipv6 to decimal +func translateToken(tempToken string, tokens []string) ([]string, error) { + if tempToken == "" { + return tokens, nil + } + + if isFloat(tempToken) { + tokens = append(tokens, tempToken) + } else if StrIsIPv4Str(tempToken) { + // Convert ipv4 to decimal then append to tokens + ip := net.ParseIP(tempToken) + decimalIP := IP4toInt(ip) + res := strconv.FormatInt(decimalIP, 10) + tokens = append(tokens, res) + + } else if StrIsIPv6Str(tempToken) { + ip := net.ParseIP(tempToken) + decimalIP := IP6toInt(ip) + tokens = append(tokens, decimalIP.String()) + } else { + return []string{}, ErrInvalidInput + } + return tokens, nil +} + +func isValidPartOfOperand(char rune) bool { + validChars := `^[0-9a-fA-F:\.]*$` + validCharsRegx := regexp.MustCompile(validChars) + return validCharsRegx.MatchString(string(char)) +} + +// TokenizeInfix Function to tokenize infix expression +func TokenizeInfix(infix string) ([]string, error) { + var tokens []string + var err error + var prevCharIsPartOfOperand bool + var prevCharIsClosingBracket bool + + infix = "(" + infix + ")" + tempToken := "" + for _, char := range infix { + opchar := string(char) + if isValidPartOfOperand(char) { + tempToken = tempToken + opchar + prevCharIsPartOfOperand = true + // If previous character was closing bracket and current character is part of operand + // then insert a '*' between them + // For example: (3+4)2 should be (3+4)*2 + if prevCharIsClosingBracket { + tokens = append(tokens, "*") + } + prevCharIsClosingBracket = false + + } else if char == '(' || char == ')' || isOperator(opchar) { + tokens, err = translateToken(tempToken, tokens) + if err != nil { + return []string{}, err + } + // If previous character was part of operand and current character is '(' + // then insert a '*' between them + // For example: 2(3+4) should be 2*(3+4) + if prevCharIsPartOfOperand && char == '(' { + tokens = append(tokens, "*") + } + + tokens = append(tokens, opchar) + tempToken = "" + + prevCharIsPartOfOperand = false + prevCharIsClosingBracket = char == ')' + } + } + tokens = append(tokens, tempToken) + return tokens, nil +} + +// IsInvalidInfix Function to check if infix expression is valid +func IsInvalidInfix(expression string) bool { + validChars := `^[0-9a-fA-F:\.\+\-\*\^\(\)\/ ]*$` + validCharsRegx := regexp.MustCompile(validChars) + + var PrevChar rune + var colonCount int + for _, char := range expression { + if isOperator(string(char)) && isOperator(string(PrevChar)) || + char == ')' && isOperator(string(PrevChar)) { + return true + } + if char == '.' && PrevChar == '.' { + return true + } + if char == ':' { + colonCount++ + if colonCount > 2 { + return true + } + } else { + colonCount = 0 + } + PrevChar = char + } + + if isOperator(string(PrevChar)) || PrevChar == '.' { + return true + } + + return !validCharsRegx.MatchString(expression) || !isBalanced(expression) +} + +// isBalanced Function to check if parentheses are balanced +func isBalanced(input string) bool { + var postfixStack Stack + for _, char := range input { + if char == '(' { + postfixStack.Push("(") + } else if char == ')' { + if postfixStack.IsEmpty() { + return false + } + postfixStack.Pop() + } + } + return postfixStack.IsEmpty() +} + +// digitsAfterDecimal Function to count the number of non-zero digits after the decimal point +func digitsAfterDecimal(float big.Float) int { + // Initially allowing 100 digits after decimal + str := float.Text('f', 100) + decimalIndex := strings.Index(str, ".") + + // Start counting the digits after the decimal point. + count := 0 + for i := len(str) - 1; i > decimalIndex; i-- { + if str[i] == '0' { + count++ + } else { + break + } + } + + return len(str) - (decimalIndex + 1) - count +} + +// CmdCalc Function is the handler for the "calc" command. +func CmdCalc(f CmdCalcFlags, args []string, printHelp func()) error { + if len(args) == 0 || f.Help { + printHelp() + return nil + } + + if f.NoColor { + color.NoColor = true + } + + infix := args[0] + if IsInvalidInfix(infix) { + return ErrInvalidInput + } + + tokens, err := TokenizeInfix(infix) + if err != nil { + return err + } + + postfix := InfixToPostfix(tokens) + result, err := EvaluatePostfix(postfix) + if err != nil { + return err + } + + precision := digitsAfterDecimal(*result) + resultStr := result.Text('f', precision) + fmt.Println(resultStr) + return nil +} diff --git a/lib/cmd_grepip.go b/lib/cmd_grepip.go index 13cf3f1c..24643db2 100644 --- a/lib/cmd_grepip.go +++ b/lib/cmd_grepip.go @@ -121,14 +121,12 @@ func CmdGrepIP( // prepare regexp var rexp *regexp.Regexp - rexp4 := `((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` - rexp6 := `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` if ipv == 4 { - rexp = regexp.MustCompilePOSIX(rexp4) + rexp = ipV4Regex } else if ipv == 6 { - rexp = regexp.MustCompilePOSIX(rexp6) + rexp = ipV6Regex } else { - rexp = regexp.MustCompilePOSIX(rexp4 + "|" + rexp6) + rexp = ipRegex } fmtSrc := color.New(color.FgMagenta) diff --git a/lib/cmd_tool_ip2n.go b/lib/cmd_tool_ip2n.go new file mode 100644 index 00000000..62d37449 --- /dev/null +++ b/lib/cmd_tool_ip2n.go @@ -0,0 +1,37 @@ +package lib + +import ( + "fmt" + "github.com/spf13/pflag" +) + +// CmdToolIP2nFlags are flags expected by CmdToolIP2n +type CmdToolIP2nFlags struct { + Help bool +} + +// Init initializes the common flags available to CmdToolIP2n with sensible +func (f *CmdToolIP2nFlags) Init() { + pflag.BoolVarP( + &f.Help, + "help", "h", false, + "show help.", + ) +} + +// CmdToolIP2n converts an IP address to a number +func CmdToolIP2n(f CmdToolIP2nFlags, args []string, printHelp func()) error { + if len(args) == 0 || f.Help { + printHelp() + return nil + } + + ipString := args[0] + res, err := IPtoDecimalStr(ipString) + if err != nil { + return err + } + + fmt.Println(res) + return nil +} diff --git a/lib/cmd_tool_n2ip.go b/lib/cmd_tool_n2ip.go new file mode 100644 index 00000000..e4378151 --- /dev/null +++ b/lib/cmd_tool_n2ip.go @@ -0,0 +1,67 @@ +package lib + +import ( + "errors" + "fmt" + "github.com/spf13/pflag" +) + +// CmdToolN2IPFlags are flags expected by CmdToolN2IP +type CmdToolN2IPFlags struct { + Help bool + ipv6 bool +} + +// Init initializes the common flags available to CmdToolN2IP with sensible +func (f *CmdToolN2IPFlags) Init() { + _h := "see description in --help" + pflag.BoolVarP( + &f.Help, + "help", "h", false, + "show help.", + ) + pflag.BoolVarP( + &f.ipv6, + "ipv6", "6", false, + _h, + ) +} + +// CmdToolN2IP converts a number to an IP address +func CmdToolN2IP(f CmdToolN2IPFlags, args []string, printHelp func()) error { + if len(args) == 0 || f.Help { + printHelp() + return nil + } + + expression := args[0] + if IsInvalidInfix(expression) { + return ErrInvalidInput + } + + // NOTE: n2ip also accepts an expression, hence the tokenization and evaluation. + + // Tokenize the expression + tokens, err := TokenizeInfix(expression) + if err != nil { + return err + } + + // Evaluate the postfix expression + postfix := InfixToPostfix(tokens) + result, err := EvaluatePostfix(postfix) + if err != nil { + return err + } + + // Convert to IP + // Precision should be 0 i.e. number of digits after decimal + // as ip cannot be derived from a float + res, err := DecimalStrToIP(result.Text('f', 0), f.ipv6) + if err != nil { + return errors.New("number is too large") + } + + fmt.Println(res.String()) + return nil +} diff --git a/lib/cmd_tool_n2ip6.go b/lib/cmd_tool_n2ip6.go new file mode 100644 index 00000000..d4e5f097 --- /dev/null +++ b/lib/cmd_tool_n2ip6.go @@ -0,0 +1,59 @@ +package lib + +import ( + "errors" + "fmt" + "github.com/spf13/pflag" +) + +// CmdToolN2IP6Flags are flags expected by CmdToolN2IP6 +type CmdToolN2IP6Flags struct { + Help bool +} + +// Init initializes the common flags available to CmdToolN2IP6 with sensible +func (f *CmdToolN2IP6Flags) Init() { + pflag.BoolVarP( + &f.Help, + "help", "h", false, + "show help.", + ) +} + +// CmdToolN2IP6 converts a number to an IPv6 address +func CmdToolN2IP6(f CmdToolN2IP6Flags, args []string, printHelp func()) error { + if len(args) == 0 || f.Help { + printHelp() + return nil + } + + expression := args[0] + if IsInvalidInfix(expression) { + return ErrInvalidInput + } + + // NOTE: n2ip6 also accepts an expression, hence the tokenization and evaluation. + + // Tokenize the expression + tokens, err := TokenizeInfix(expression) + if err != nil { + return err + } + + // Evaluate the postfix expression + postfix := InfixToPostfix(tokens) + result, err := EvaluatePostfix(postfix) + if err != nil { + return err + } + // Convert to IP + // Precision should be 0 i.e. number of digits after decimal + // as ip cannot be derived from a float + res, err := DecimalStrToIP(result.Text('f', 0), true) + if err != nil { + return errors.New("number is too large") + } + + fmt.Println(res.String()) + return nil +} diff --git a/lib/init.go b/lib/init.go index ae65bbf9..2f96736b 100644 --- a/lib/init.go +++ b/lib/init.go @@ -1,6 +1,11 @@ package lib +import "regexp" + func init() { bogonIP4List = GetBogonRange4() bogonIP6List = GetBogonRange6() + ipV4Regex = regexp.MustCompilePOSIX(IPv4RegexPattern) + ipV6Regex = regexp.MustCompilePOSIX(IPv6RegexPattern) + ipRegex = regexp.MustCompilePOSIX(IPv4RegexPattern + "|" + IPv6RegexPattern) } diff --git a/lib/ip_conversions.go b/lib/ip_conversions.go new file mode 100644 index 00000000..44d4fe17 --- /dev/null +++ b/lib/ip_conversions.go @@ -0,0 +1,73 @@ +package lib + +import ( + "fmt" + "math/big" + "net" + "strconv" +) + +// IPtoDecimalStr converts an IP address to a decimal string +func IPtoDecimalStr(strIP string) (string, error) { + if StrIsIPv6Str(strIP) { + ip := net.ParseIP(strIP) + if ip == nil { + return "", ErrNotIP + } + + decimalIP := IP6toInt(ip) + return decimalIP.String(), nil + } + if StrIsIPv4Str(strIP) { + ip := net.ParseIP(strIP) + if ip == nil { + return "", ErrNotIP + } + return strconv.FormatInt(IP4toInt(ip), 10), nil + } else { + return "", ErrNotIP + } +} + +// DecimalStrToIP converts a decimal string to an IP address +func DecimalStrToIP(decimal string, forceIPv6 bool) (net.IP, error) { + // Create a new big.Int with a value of 'decimal' + num := new(big.Int) + num, success := num.SetString(decimal, 10) + + if !success { + fmt.Print(decimal) + return nil, ErrInvalidInput + } + // Convert to IPv4 if not forcing IPv6 and 'num' is within the IPv4 range + if !forceIPv6 && num.Cmp(big.NewInt(4294967295)) <= 0 { + ip := make(net.IP, 4) + b := num.Bytes() + copy(ip[4-len(b):], b) + return ip, nil + } + // Convert to IPv6 if 'num' is within the IPv6 range + maxIpv6 := new(big.Int) + maxIpv6.SetString("340282366920938463463374607431768211455", 10) + if num.Cmp(maxIpv6) <= 0 { + ip := make(net.IP, 16) + b := num.Bytes() + copy(ip[16-len(b):], b) + return ip, nil + } + return nil, ErrInvalidInput +} + +// IP6toInt converts an IPv6 address to a big.Int +func IP6toInt(IPv6Address net.IP) *big.Int { + IPv6Int := big.NewInt(0) + IPv6Int.SetBytes(IPv6Address) + return IPv6Int +} + +// IP4toInt converts an IPv4 address to a big.Int +func IP4toInt(IPv4Address net.IP) int64 { + IPv4Int := big.NewInt(0) + IPv4Int.SetBytes(IPv4Address.To4()) + return IPv4Int.Int64() +} diff --git a/lib/ip_str.go b/lib/ip_str.go index 7dcc24a8..2b67772f 100644 --- a/lib/ip_str.go +++ b/lib/ip_str.go @@ -8,3 +8,13 @@ import ( func StrIsIPStr(ipStr string) bool { return net.ParseIP(ipStr) != nil } + +// StrIsIPv4Str checks if the given string is an IPv4 address +func StrIsIPv4Str(expression string) bool { + return ipV4Regex.MatchString(expression) +} + +// StrIsIPv6Str checks if the given string is an IPv6 address +func StrIsIPv6Str(expression string) bool { + return ipV6Regex.MatchString(expression) +} diff --git a/lib/regex.go b/lib/regex.go new file mode 100644 index 00000000..474524c2 --- /dev/null +++ b/lib/regex.go @@ -0,0 +1,10 @@ +package lib + +import "regexp" + +var ipV4Regex *regexp.Regexp +var ipV6Regex *regexp.Regexp +var ipRegex *regexp.Regexp + +const IPv4RegexPattern = `((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` +const IPv6RegexPattern = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` diff --git a/lib/stack.go b/lib/stack.go new file mode 100644 index 00000000..f661aa68 --- /dev/null +++ b/lib/stack.go @@ -0,0 +1,33 @@ +package lib + +// Stack represents a stack data structure that holds a collection of strings. +// The elements in the stack are managed using the Last-In-First-Out (LIFO) principle, +// which means that the last element added to the stack will be the first one to be removed. +type Stack []string + +// IsEmpty checks if the stack is empty. +// It returns true if the stack contains no elements (i.e., it is empty), and false otherwise. +func (st *Stack) IsEmpty() bool { + return len(*st) == 0 +} + +// Push adds a new value onto the stack. +// The new value (given as a string) is appended to the top of the stack, effectively making it the new top element. +func (st *Stack) Push(str string) { + *st = append(*st, str) //Simply append the new value to the end of the stack +} + +// Pop removes the top element from the stack and returns it. If the stack is empty, +// it returns an empty string and true indicating that the stack is empty. +// Otherwise, it returns the top element of the stack and false. +// The stack is modified in-place by removing the top element. +func (st *Stack) Pop() (string, bool) { + if st.IsEmpty() { + return "", true + } else { + index := len(*st) - 1 // Get the index of top most element. + element := (*st)[index] // Index onto the slice and obtain the element. + *st = (*st)[:index] // Remove it from the stack by slicing it off. + return element, false + } +}