diff --git a/node_interfaces/interface_core_string-decode_v1.go b/node_interfaces/interface_core_string-decode_v1.go new file mode 100644 index 0000000..229174d --- /dev/null +++ b/node_interfaces/interface_core_string-decode_v1.go @@ -0,0 +1,17 @@ +// Code generated by actrun. DO NOT EDIT. + +package node_interfaces + +import "github.com/actionforge/actrun-cli/core" // Decode a string from various formats like Base64, Hex, UTF-16, UTF-32, or unescape HTML, URL, JSON, and XML sequences. + +// ==> (o) Inputs + +// The encoded string to decode. +const Core_string_decode_v1_Input_input core.InputId = "input" +// The decoding operation to apply. +const Core_string_decode_v1_Input_op core.InputId = "op" + +// Outputs (o) ==> + +// The resulting decoded string. +const Core_string_decode_v1_Output_result core.OutputId = "result" diff --git a/node_interfaces/interface_core_string-encode_v1.go b/node_interfaces/interface_core_string-encode_v1.go index d41fdae..3f4e1dd 100644 --- a/node_interfaces/interface_core_string-encode_v1.go +++ b/node_interfaces/interface_core_string-encode_v1.go @@ -2,7 +2,7 @@ package node_interfaces -import "github.com/actionforge/actrun-cli/core" // Encode a UTF-8 string into various string-based formats like Base64, Hex, UTF-16, and UTF-32. +import "github.com/actionforge/actrun-cli/core" // Encode a UTF-8 string into various string-based formats like Base64, Hex, UTF-16, UTF-32, or apply escape sequences for HTML, URL, JSON, and XML. // ==> (o) Inputs diff --git a/nodes/string-decode@v1.go b/nodes/string-decode@v1.go new file mode 100644 index 0000000..88fece1 --- /dev/null +++ b/nodes/string-decode@v1.go @@ -0,0 +1,204 @@ +package nodes + +import ( + _ "embed" + "encoding/base32" + "encoding/base64" + "encoding/hex" + "html" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/actionforge/actrun-cli/core" + ni "github.com/actionforge/actrun-cli/node_interfaces" + + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/encoding/unicode/utf32" +) + +//go:embed string-decode@v1.yml +var stringDecodeDefinition string + +type StringDecode struct { + core.NodeBaseComponent + core.Inputs + core.Outputs +} + +func (n *StringDecode) OutputValueById(c *core.ExecutionState, outputId core.OutputId) (any, error) { + input, err := core.InputValueById[string](c, n, ni.Core_string_decode_v1_Input_input) + if err != nil { + return nil, err + } + + op, err := core.InputValueById[string](c, n, ni.Core_string_decode_v1_Input_op) + if err != nil { + return nil, err + } + + var result string + inputBytes := []byte(input) + + switch op { + case "base64": + decoded, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode base64") + } + result = string(decoded) + case "base64url": + decoded, err := base64.URLEncoding.DecodeString(input) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode base64url") + } + result = string(decoded) + case "base32": + decoded, err := base32.StdEncoding.DecodeString(input) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode base32") + } + result = string(decoded) + case "hex": + decoded, err := hex.DecodeString(input) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode hex") + } + result = string(decoded) + + case "utf8": + result = input // No-op, it's already a UTF-8 string + case "utf16le": + decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder() + utf8Bytes, err := decoder.Bytes(inputBytes) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode utf16le") + } + result = string(utf8Bytes) + case "utf16be": + decoder := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder() + utf8Bytes, err := decoder.Bytes(inputBytes) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode utf16be") + } + result = string(utf8Bytes) + case "utf32le": + decoder := utf32.UTF32(utf32.LittleEndian, utf32.IgnoreBOM).NewDecoder() + utf8Bytes, err := decoder.Bytes(inputBytes) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode utf32le") + } + result = string(utf8Bytes) + case "utf32be": + decoder := utf32.UTF32(utf32.BigEndian, utf32.IgnoreBOM).NewDecoder() + utf8Bytes, err := decoder.Bytes(inputBytes) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode utf32be") + } + result = string(utf8Bytes) + + case "html": + result = html.UnescapeString(input) + case "url": + decoded, err := url.QueryUnescape(input) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode url") + } + result = decoded + case "urlpath": + decoded, err := url.PathUnescape(input) + if err != nil { + return nil, core.CreateErr(c, err, "failed to decode url path") + } + result = decoded + case "json": + result = unescapeJSON(input) + case "xml": + result = unescapeXML(input) + default: + return nil, core.CreateErr(c, nil, "unknown operation '%s'", op) + } + + return result, nil +} + +var jsonEscapeRegex = regexp.MustCompile(`\\(["\\bfnrt/]|u[0-9a-fA-F]{4})`) + +func unescapeJSON(s string) string { + return jsonEscapeRegex.ReplaceAllStringFunc(s, func(match string) string { + switch match { + case `\"`: + return `"` + case `\\`: + return `\` + case `\b`: + return "\b" + case `\f`: + return "\f" + case `\n`: + return "\n" + case `\r`: + return "\r" + case `\t`: + return "\t" + case `\/`: + return "/" + default: + // Handle \uXXXX + if strings.HasPrefix(match, `\u`) && len(match) == 6 { + code, err := strconv.ParseInt(match[2:], 16, 32) + if err == nil { + return string(rune(code)) + } + } + return match + } + }) +} + +func unescapeXML(s string) string { + replacements := []struct { + escaped string + unescaped string + }{ + {"<", "<"}, + {">", ">"}, + {"&", "&"}, + {"'", "'"}, + {""", `"`}, + } + + result := s + for _, r := range replacements { + result = strings.ReplaceAll(result, r.escaped, r.unescaped) + } + + // Handle numeric character references like < or < + numericRegex := regexp.MustCompile(`&#(x[0-9a-fA-F]+|\d+);`) + result = numericRegex.ReplaceAllStringFunc(result, func(match string) string { + inner := match[2 : len(match)-1] + var code int64 + var err error + if strings.HasPrefix(inner, "x") { + code, err = strconv.ParseInt(inner[1:], 16, 32) + } else { + code, err = strconv.ParseInt(inner, 10, 32) + } + if err == nil { + return string(rune(code)) + } + return match + }) + + return result +} + +func init() { + err := core.RegisterNodeFactory(stringDecodeDefinition, func(ctx any, parent core.NodeBaseInterface, parentId string, nodeDef map[string]any, validate bool) (core.NodeBaseInterface, []error) { + return &StringDecode{}, nil + }) + if err != nil { + panic(err) + } +} diff --git a/nodes/string-decode@v1.yml b/nodes/string-decode@v1.yml new file mode 100644 index 0000000..8c91cfc --- /dev/null +++ b/nodes/string-decode@v1.yml @@ -0,0 +1,71 @@ +yaml-version: 3.0 +id: core/string-decode +name: String Decode +category: processing +icon: tablerHash +version: 1 +style: + header: + background: "#6e6e6e" + body: + background: "#423f3f" +short_desc: Decode a string from various formats like Base64, Hex, UTF-16, UTF-32, or unescape HTML, URL, JSON, and XML sequences. +addendum: | + ## Decoding Operations + + This node takes an encoded string and decodes it back to UTF-8. + + - **Printable Formats (Base64, Hex, etc.):** + These operations decode a human-readable ASCII string back to its original form. + + - **Raw Byte Formats (UTF-16, UTF-32):** + These operations decode a string containing raw encoded bytes back to UTF-8. + The input is expected to be produced by the `String Encode` node. + + - **Unescape Formats (HTML, URL, JSON, XML):** + These operations unescape special character sequences in the input string + to restore the original characters. +outputs: + result: + type: string + index: 0 + desc: The resulting decoded string. +inputs: + input: + name: Input + type: string + index: 0 + desc: The encoded string to decode. + op: + name: Decoding + type: option + index: 1 + default: base64 + desc: The decoding operation to apply. + options: + - name: From Base16 (Hex) + value: hex + - name: From Base32 + value: base32 + - name: From Base64 + value: base64 + - name: From Base64 (URL Safe) + value: base64url + - name: From UTF-16 LE + value: utf16le + - name: From UTF-16 BE + value: utf16be + - name: From UTF-32 LE + value: utf32le + - name: From UTF-32 BE + value: utf32be + - name: From HTML Unescape + value: html + - name: From URL Decode (Query) + value: url + - name: From URL Decode (Path) + value: urlpath + - name: From JSON Unescape + value: json + - name: From XML Unescape + value: xml diff --git a/nodes/string-encode@v1.go b/nodes/string-encode@v1.go index 73d2333..834932b 100644 --- a/nodes/string-encode@v1.go +++ b/nodes/string-encode@v1.go @@ -5,6 +5,10 @@ import ( "encoding/base32" "encoding/base64" "encoding/hex" + "fmt" + "html" + "net/url" + "strings" "github.com/actionforge/actrun-cli/core" ni "github.com/actionforge/actrun-cli/node_interfaces" @@ -76,6 +80,17 @@ func (n *StringEncode) OutputValueById(c *core.ExecutionState, outputId core.Out return nil, core.CreateErr(c, err, "failed to encode utf32be") } result = string(utf32Bytes) // Return raw bytes as a string + + case "html": + result = html.EscapeString(input) + case "url": + result = url.QueryEscape(input) + case "urlpath": + result = url.PathEscape(input) + case "json": + result = escapeJSON(input) + case "xml": + result = escapeXML(input) default: return nil, core.CreateErr(c, nil, "unknown operation '%s'", op) } @@ -83,6 +98,56 @@ func (n *StringEncode) OutputValueById(c *core.ExecutionState, outputId core.Out return result, nil } +func escapeJSON(s string) string { + var b strings.Builder + for _, r := range s { + switch r { + case '"': + b.WriteString(`\"`) + case '\\': + b.WriteString(`\\`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + b.WriteString(fmt.Sprintf(`\u%04x`, r)) + } else { + b.WriteRune(r) + } + } + } + return b.String() +} + +func escapeXML(s string) string { + var b strings.Builder + for _, r := range s { + switch r { + case '<': + b.WriteString("<") + case '>': + b.WriteString(">") + case '&': + b.WriteString("&") + case '\'': + b.WriteString("'") + case '"': + b.WriteString(""") + default: + b.WriteRune(r) + } + } + return b.String() +} + func init() { err := core.RegisterNodeFactory(stringEncodeDefinition, func(ctx any, parent core.NodeBaseInterface, parentId string, nodeDef map[string]any, validate bool) (core.NodeBaseInterface, []error) { return &StringEncode{}, nil diff --git a/nodes/string-encode@v1.yml b/nodes/string-encode@v1.yml index e221092..533fb24 100644 --- a/nodes/string-encode@v1.yml +++ b/nodes/string-encode@v1.yml @@ -9,7 +9,7 @@ style: background: "#6e6e6e" body: background: "#423f3f" -short_desc: Encode a UTF-8 string into various string-based formats like Base64, Hex, UTF-16, and UTF-32. +short_desc: Encode a UTF-8 string into various string-based formats like Base64, Hex, UTF-16, UTF-32, or apply escape sequences for HTML, URL, JSON, and XML. addendum: | ## Encoding Operations @@ -22,7 +22,10 @@ addendum: | These operations produce a string object containing the raw encoded bytes. **This output will not be human-readable** and is intended to be used with the `String Decode` node. -compact: true + + - **Escape Formats (HTML, URL, JSON, XML):** + These operations escape special characters in the input string to make + them safe for use in the respective context. outputs: result: type: string @@ -41,19 +44,29 @@ inputs: default: base64 desc: The encoding operation to apply. options: - - name: Base16 (Hex) + - name: To Base16 (Hex) value: hex - - name: Base32 + - name: To Base32 value: base32 - - name: Base64 + - name: To Base64 value: base64 - - name: Base64 (URL Safe) + - name: To Base64 (URL Safe) value: base64url - - name: UTF-16 LE + - name: To UTF-16 LE value: utf16le - - name: UTF-16 BE + - name: To UTF-16 BE value: utf16be - - name: UTF-32 LE + - name: To UTF-32 LE value: utf32le - - name: UTF-32 BE - value: utf32be \ No newline at end of file + - name: To UTF-32 BE + value: utf32be + - name: To HTML Escape + value: html + - name: To URL Encode (Query) + value: url + - name: To URL Encode (Path) + value: urlpath + - name: To JSON Escape + value: json + - name: To XML Escape + value: xml \ No newline at end of file