Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions node_interfaces/interface_core_string-decode_v1.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion node_interfaces/interface_core_string-encode_v1.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

204 changes: 204 additions & 0 deletions nodes/string-decode@v1.go
Original file line number Diff line number Diff line change
@@ -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
}{
{"&lt;", "<"},
{"&gt;", ">"},
{"&amp;", "&"},
{"&apos;", "'"},
{"&quot;", `"`},
}

result := s
for _, r := range replacements {
result = strings.ReplaceAll(result, r.escaped, r.unescaped)
}

// Handle numeric character references like &#60; or &#x3C;
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)
}
}
71 changes: 71 additions & 0 deletions nodes/string-decode@v1.yml
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions nodes/string-encode@v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -76,13 +80,74 @@ 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)
}

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("&lt;")
case '>':
b.WriteString("&gt;")
case '&':
b.WriteString("&amp;")
case '\'':
b.WriteString("&apos;")
case '"':
b.WriteString("&quot;")
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
Expand Down
Loading
Loading