From 88dc0b43868e13645217c93f084ea7419adc2837 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Sat, 6 Jan 2024 09:00:42 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20parse.duration=20(#2956)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This now supports parsing durations in seconds (s), minutes (m), hours (m), days (d), and years (y). This works with both the shorthand scalar as well as the long version, e.g.: ```coffee parse.duration("3d") parse.duration("3days") ``` The parser is very lenient, but we recommend using the above scalars as so: ```coffee 1s = 1 second 2m = 2 minutes 3h = 3 hours 4d = 4 days 5y = 5 years ``` Signed-off-by: Dominik Richter --- llx/builtin.go | 3 +- llx/builtin_resource.go | 41 ++++++++++++++++++++++++++ mqlc/builtin.go | 3 +- mqlc/builtin_resource.go | 38 ++++++++++++++++++++++++ mqlc/mqlc_test.go | 2 +- providers/core/resources/core.lr | 1 + providers/core/resources/parse_test.go | 41 ++++++++++++++++++++++++++ 7 files changed, 126 insertions(+), 3 deletions(-) diff --git a/llx/builtin.go b/llx/builtin.go index af97c461fc..90b38de341 100644 --- a/llx/builtin.go +++ b/llx/builtin.go @@ -694,7 +694,8 @@ func init() { return e.runBlock(bind, chunk.Function.Args[0], chunk.Function.Args[1:], ref) }}, // TODO: [#32] unique builtin fields that need a long-term support in LR - string(types.Resource("parse") + ".date"): {f: resourceDateV2}, + string(types.Resource("parse") + ".date"): {f: resourceDateV2}, + string(types.Resource("parse") + ".duration"): {f: resourceDuration}, }, } diff --git a/llx/builtin_resource.go b/llx/builtin_resource.go index c58bced601..3a9933825f 100644 --- a/llx/builtin_resource.go +++ b/llx/builtin_resource.go @@ -5,7 +5,9 @@ package llx import ( "errors" + "regexp" "strconv" + "strings" "time" "go.mondoo.com/cnquery/v9/types" @@ -282,3 +284,42 @@ func resourceDateV2(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) ( return nil, 0, errors.New("failed to parse time") } + +var durationRegex = regexp.MustCompile(`^(\d+|[.])(\w*)$`) + +func resourceDuration(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) { + args, rref, err := primitive2array(e, ref, chunk.Function.Args) + if err != nil || rref != 0 { + return nil, rref, err + } + + // Note: Using the regex is slower than parsing it step by step, so this code can be improved + m := durationRegex.FindStringSubmatch(args[0].(string)) + if m == nil { + return nil, 0, errors.New("failed to parse duration") + } + + num, err := strconv.ParseFloat(m[1], 64) + if err != nil { + return nil, 0, errors.New("failed to parse duration numeric value") + } + + var t time.Time + scalar := strings.ToLower(m[2]) + switch scalar { + case "s", "", "sec", "second", "seconds": + t = DurationToTime(int64(num)) + case "m", "min", "minute", "minutes": + t = DurationToTime(int64(num * 60)) + case "h", "hour", "hours": + t = DurationToTime(int64(num * 60 * 60)) + case "d", "day", "days": + t = DurationToTime(int64(num * 60 * 60 * 24)) + case "y", "year", "years": + t = DurationToTime(int64(num * 60 * 60 * 24 * 365)) + default: + return nil, 0, errors.New("failed to parsee duration (only supports: s/m/h/d/y)") + } + + return TimeData(t), 0, nil +} diff --git a/mqlc/builtin.go b/mqlc/builtin.go index 51f9df6799..7f35f3b8a5 100644 --- a/mqlc/builtin.go +++ b/mqlc/builtin.go @@ -135,7 +135,8 @@ func init() { }, // TODO: [#32] unique builtin fields that need a long-term support in LR types.Resource("parse"): { - "date": {compile: compileResourceParseDate, signature: FunctionSignature{Required: 1, Args: []types.Type{types.String, types.String}}}, + "date": {compile: compileResourceParseDate, signature: FunctionSignature{Required: 1, Args: []types.Type{types.String, types.String}}}, + "duration": {compile: compileResourceParseDuration, signature: FunctionSignature{Required: 1, Args: []types.Type{types.String}}}, }, } } diff --git a/mqlc/builtin_resource.go b/mqlc/builtin_resource.go index 987a4ccb2d..81aff14c00 100644 --- a/mqlc/builtin_resource.go +++ b/mqlc/builtin_resource.go @@ -505,3 +505,41 @@ func compileResourceParseDate(c *compiler, typ types.Type, ref uint64, id string }) return types.Time, nil } + +func compileResourceParseDuration(c *compiler, typ types.Type, ref uint64, id string, call *parser.Call) (types.Type, error) { + if call == nil { + return types.Nil, errors.New("missing arguments to parse duration") + } + + functionID := string(typ) + "." + id + + init := &resources.Init{ + Args: []*resources.TypedArg{ + {Name: "value", Type: string(types.String)}, + }, + } + args, err := c.unnamedArgs("parse."+id, init, call.Function) + if err != nil { + return types.Nil, err + } + + rawArgs := make([]*llx.Primitive, len(call.Function)) + for i := range call.Function { + rawArgs[i] = args[i*2+1] + } + + if len(rawArgs) == 0 { + return types.Nil, errors.New("missing arguments to parse duration") + } + + c.addChunk(&llx.Chunk{ + Call: llx.Chunk_FUNCTION, + Id: functionID, + Function: &llx.Function{ + Type: string(types.Time), + Binding: ref, + Args: rawArgs, + }, + }) + return types.Time, nil +} diff --git a/mqlc/mqlc_test.go b/mqlc/mqlc_test.go index ee29a79ebb..645e23dbb3 100644 --- a/mqlc/mqlc_test.go +++ b/mqlc/mqlc_test.go @@ -1954,7 +1954,7 @@ func TestSuggestions(t *testing.T) { { // builtin calls "parse.d", - []string{"date"}, + []string{"date", "duration"}, errors.New("cannot find field 'd' in parse"), nil, }, diff --git a/providers/core/resources/core.lr b/providers/core/resources/core.lr index 478161c364..8f84ecdb08 100644 --- a/providers/core/resources/core.lr +++ b/providers/core/resources/core.lr @@ -102,6 +102,7 @@ regex { parse { // Built-in functions: // date(value, format) time + // duration(value) time } // UUIDs based on RFC 4122 and DCE 1.1 diff --git a/providers/core/resources/parse_test.go b/providers/core/resources/parse_test.go index ffcff2cd8d..f5e03934dd 100644 --- a/providers/core/resources/parse_test.go +++ b/providers/core/resources/parse_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "go.mondoo.com/cnquery/v9/llx" "go.mondoo.com/cnquery/v9/providers-sdk/v1/testutils" ) @@ -34,3 +35,43 @@ func TestParse_Date(t *testing.T) { }, }) } + +func TestParse_Duration(t *testing.T) { + twoSecs := llx.DurationToTime(2) + tenMin := llx.DurationToTime(10 * 60) + threeHours := llx.DurationToTime(3 * 60 * 60) + thirtyDays := llx.DurationToTime(30 * 60 * 60 * 24) + sevenYears := llx.DurationToTime(7 * 60 * 60 * 24 * 365) + x.TestSimple(t, []testutils.SimpleTest{ + { + Code: "parse.duration('2')", + ResultIndex: 0, + Expectation: &twoSecs, + }, + { + Code: "parse.duration('2seconds')", + ResultIndex: 0, + Expectation: &twoSecs, + }, + { + Code: "parse.duration('10min')", + ResultIndex: 0, + Expectation: &tenMin, + }, + { + Code: "parse.duration('3h')", + ResultIndex: 0, + Expectation: &threeHours, + }, + { + Code: "parse.duration('30day')", + ResultIndex: 0, + Expectation: &thirtyDays, + }, + { + Code: "parse.duration('7y')", + ResultIndex: 0, + Expectation: &sevenYears, + }, + }) +}