lib/file.go (134 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package lib import ( "bytes" "io" "os" "sort" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" ) // File returns a cel.EnvOption to configure extended functions for reading files. // It takes a mapping of mimetypes to transforms to allow reading specific mime // type. The values in the map must be one of: func([]byte), func(io.Reader) io.Reader, // func(io.Reader) (io.Reader, error) or func(io.Reader) ref.Val. If the // transform is func([]byte) it is expected to mutate the bytes in place. // // # Dir // // dir returns either a directory for the provided path: // // dir(<string>) -> <list<map<string,dyn>>> // // Examples: // // dir('subdir') // // will return something like: // // [ // { // "is_dir": true, // "mod_time": "2022-04-05T20:53:11.923840504+09:30", // "name": "subsubdir", // "size": 4096 // }, // { // "is_dir": false, // "mod_time": "2022-04-05T20:53:11.923840504+09:30", // "name": "a.txt", // "size": 13 // }, // { // "is_dir": false, // "mod_time": "2022-04-05T20:53:11.923840504+09:30", // "name": "b.txt", // "size": 11 // } // ] // // # File // // file returns either a <bytes> or a <dyn> depending on whether it is called // with one parameter or two: // // file(<string>) -> <bytes> // file(<string>, <string>) -> <dyn> // // The first parameter is a file path and the second is a look-up into the // transforms map provided by to the File cel.EnvOption. // // Examples: // // Given a file hello.txt: // world! // // And the following transforms map (rot13 is a transforming reader): // // map[string]interface{}{ // "text/rot13": func(r io.Reader) io.Reader { return rot13{r} }, // "text/upper": func(p []byte) { // for i, b := range p { // if 'a' <= b && b <= 'z' { // p[i] &^= 'a' - 'A' // } // } // }, // } // // string(file('hello.txt')) // return "world!\n" // string(file('hello.txt', 'text/rot13')) // return "jbeyq!\n" // string(file('hello.txt', 'text/upper')) // return "WORLD!\n" func File(mimetypes map[string]interface{}) cel.EnvOption { return cel.Lib(fileLib{transforms: mimetypes}) } type fileLib struct { transforms map[string]interface{} } func (l fileLib) CompileOptions() []cel.EnvOption { return []cel.EnvOption{ cel.Function("dir", cel.Overload( "dir_string", []*cel.Type{cel.StringType}, cel.ListType(mapStringDyn), cel.UnaryBinding(catch(readDir)), ), ), cel.Function("file", cel.Overload( "file_string", []*cel.Type{cel.StringType}, cel.BytesType, cel.UnaryBinding(catch(readFile)), ), cel.Overload( "file_string_string", []*cel.Type{cel.StringType, cel.StringType}, cel.DynType, cel.BinaryBinding(catch(l.readMIMEFile)), ), ), } } func (fileLib) ProgramOptions() []cel.ProgramOption { return nil } func readDir(arg ref.Val) ref.Val { path, ok := arg.(types.String) if !ok { return types.ValOrErr(path, "no such overload for dir: %s", arg.Type()) } f, err := os.Open(string(path)) if err != nil { return types.NewErr("dir: %v", err) } dir, err := f.ReadDir(0) if err != nil { return types.NewErr("dir: %v", err) } // Stabilise order across platforms. sort.Slice(dir, func(i, j int) bool { return dir[i].Name() < dir[j].Name() }) res := make([]map[string]interface{}, len(dir)) for i, e := range dir { fi, err := e.Info() if err != nil { return types.NewErr("dir: %v", err) } res[i] = map[string]interface{}{ "name": e.Name(), "is_dir": e.IsDir(), "size": fi.Size(), "mod_time": fi.ModTime(), } } return types.NewDynamicList(types.DefaultTypeAdapter, res) } func readFile(arg ref.Val) ref.Val { path, ok := arg.(types.String) if !ok { return types.ValOrErr(path, "no such overload for file: %s", arg.Type()) } b, err := os.ReadFile(string(path)) if err != nil { return types.NewErr("file: %v", err) } return types.Bytes(b) } func (l fileLib) readMIMEFile(arg0, arg1 ref.Val) ref.Val { path, ok := arg0.(types.String) if !ok { return types.ValOrErr(path, "no such overload for file path: %s", arg0.Type()) } mimetype, ok := arg1.(types.String) if !ok { return types.ValOrErr(mimetype, "no such overload for mime type: %s", arg1.Type()) } transform, ok := l.transforms[string(mimetype)] if !ok { return types.NewErr("unknown transform: %q", mimetype) } f, err := os.Open(string(path)) if err != nil { return types.NewErr("file: %v", err) } defer f.Close() switch transform := transform.(type) { case func([]byte): var buf bytes.Buffer _, err := io.Copy(&buf, transformReader{ r: f, transform: transform, }) if err != nil { return types.NewErr("file: %v", err) } return types.Bytes(buf.Bytes()) case func(io.Reader) io.Reader: var buf bytes.Buffer _, err := io.Copy(&buf, transform(f)) if err != nil { return types.NewErr("file: %v", err) } return types.Bytes(buf.Bytes()) case func(io.Reader) (io.Reader, error): var buf bytes.Buffer r, err := transform(f) if err != nil { return types.NewErr("file: %v", err) } _, err = io.Copy(&buf, r) if err != nil { return types.NewErr("file: %v", err) } return types.Bytes(buf.Bytes()) case func(io.Reader) ref.Val: return transform(f) } return types.NewErr("invalid transform: %T", transform) }