Skip to content

Commit 7c621b5

Browse files
committed
Add core/string-decode and add a few more options to core/string-encode
1 parent 408b3bd commit 7c621b5

6 files changed

Lines changed: 382 additions & 12 deletions

File tree

node_interfaces/interface_core_string-decode_v1.go

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_interfaces/interface_core_string-encode_v1.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodes/string-decode@v1.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package nodes
2+
3+
import (
4+
_ "embed"
5+
"encoding/base32"
6+
"encoding/base64"
7+
"encoding/hex"
8+
"html"
9+
"net/url"
10+
"regexp"
11+
"strconv"
12+
"strings"
13+
14+
"github.com/actionforge/actrun-cli/core"
15+
ni "github.com/actionforge/actrun-cli/node_interfaces"
16+
17+
"golang.org/x/text/encoding/unicode"
18+
"golang.org/x/text/encoding/unicode/utf32"
19+
)
20+
21+
//go:embed string-decode@v1.yml
22+
var stringDecodeDefinition string
23+
24+
type StringDecode struct {
25+
core.NodeBaseComponent
26+
core.Inputs
27+
core.Outputs
28+
}
29+
30+
func (n *StringDecode) OutputValueById(c *core.ExecutionState, outputId core.OutputId) (any, error) {
31+
input, err := core.InputValueById[string](c, n, ni.Core_string_decode_v1_Input_input)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
op, err := core.InputValueById[string](c, n, ni.Core_string_decode_v1_Input_op)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
var result string
42+
inputBytes := []byte(input)
43+
44+
switch op {
45+
case "base64":
46+
decoded, err := base64.StdEncoding.DecodeString(input)
47+
if err != nil {
48+
return nil, core.CreateErr(c, err, "failed to decode base64")
49+
}
50+
result = string(decoded)
51+
case "base64url":
52+
decoded, err := base64.URLEncoding.DecodeString(input)
53+
if err != nil {
54+
return nil, core.CreateErr(c, err, "failed to decode base64url")
55+
}
56+
result = string(decoded)
57+
case "base32":
58+
decoded, err := base32.StdEncoding.DecodeString(input)
59+
if err != nil {
60+
return nil, core.CreateErr(c, err, "failed to decode base32")
61+
}
62+
result = string(decoded)
63+
case "hex":
64+
decoded, err := hex.DecodeString(input)
65+
if err != nil {
66+
return nil, core.CreateErr(c, err, "failed to decode hex")
67+
}
68+
result = string(decoded)
69+
70+
case "utf8":
71+
result = input // No-op, it's already a UTF-8 string
72+
case "utf16le":
73+
decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
74+
utf8Bytes, err := decoder.Bytes(inputBytes)
75+
if err != nil {
76+
return nil, core.CreateErr(c, err, "failed to decode utf16le")
77+
}
78+
result = string(utf8Bytes)
79+
case "utf16be":
80+
decoder := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder()
81+
utf8Bytes, err := decoder.Bytes(inputBytes)
82+
if err != nil {
83+
return nil, core.CreateErr(c, err, "failed to decode utf16be")
84+
}
85+
result = string(utf8Bytes)
86+
case "utf32le":
87+
decoder := utf32.UTF32(utf32.LittleEndian, utf32.IgnoreBOM).NewDecoder()
88+
utf8Bytes, err := decoder.Bytes(inputBytes)
89+
if err != nil {
90+
return nil, core.CreateErr(c, err, "failed to decode utf32le")
91+
}
92+
result = string(utf8Bytes)
93+
case "utf32be":
94+
decoder := utf32.UTF32(utf32.BigEndian, utf32.IgnoreBOM).NewDecoder()
95+
utf8Bytes, err := decoder.Bytes(inputBytes)
96+
if err != nil {
97+
return nil, core.CreateErr(c, err, "failed to decode utf32be")
98+
}
99+
result = string(utf8Bytes)
100+
101+
case "html":
102+
result = html.UnescapeString(input)
103+
case "url":
104+
decoded, err := url.QueryUnescape(input)
105+
if err != nil {
106+
return nil, core.CreateErr(c, err, "failed to decode url")
107+
}
108+
result = decoded
109+
case "urlpath":
110+
decoded, err := url.PathUnescape(input)
111+
if err != nil {
112+
return nil, core.CreateErr(c, err, "failed to decode url path")
113+
}
114+
result = decoded
115+
case "json":
116+
result = unescapeJSON(input)
117+
case "xml":
118+
result = unescapeXML(input)
119+
default:
120+
return nil, core.CreateErr(c, nil, "unknown operation '%s'", op)
121+
}
122+
123+
return result, nil
124+
}
125+
126+
var jsonEscapeRegex = regexp.MustCompile(`\\(["\\bfnrt/]|u[0-9a-fA-F]{4})`)
127+
128+
func unescapeJSON(s string) string {
129+
return jsonEscapeRegex.ReplaceAllStringFunc(s, func(match string) string {
130+
switch match {
131+
case `\"`:
132+
return `"`
133+
case `\\`:
134+
return `\`
135+
case `\b`:
136+
return "\b"
137+
case `\f`:
138+
return "\f"
139+
case `\n`:
140+
return "\n"
141+
case `\r`:
142+
return "\r"
143+
case `\t`:
144+
return "\t"
145+
case `\/`:
146+
return "/"
147+
default:
148+
// Handle \uXXXX
149+
if strings.HasPrefix(match, `\u`) && len(match) == 6 {
150+
code, err := strconv.ParseInt(match[2:], 16, 32)
151+
if err == nil {
152+
return string(rune(code))
153+
}
154+
}
155+
return match
156+
}
157+
})
158+
}
159+
160+
func unescapeXML(s string) string {
161+
replacements := []struct {
162+
escaped string
163+
unescaped string
164+
}{
165+
{"&lt;", "<"},
166+
{"&gt;", ">"},
167+
{"&amp;", "&"},
168+
{"&apos;", "'"},
169+
{"&quot;", `"`},
170+
}
171+
172+
result := s
173+
for _, r := range replacements {
174+
result = strings.ReplaceAll(result, r.escaped, r.unescaped)
175+
}
176+
177+
// Handle numeric character references like &#60; or &#x3C;
178+
numericRegex := regexp.MustCompile(`&#(x[0-9a-fA-F]+|\d+);`)
179+
result = numericRegex.ReplaceAllStringFunc(result, func(match string) string {
180+
inner := match[2 : len(match)-1]
181+
var code int64
182+
var err error
183+
if strings.HasPrefix(inner, "x") {
184+
code, err = strconv.ParseInt(inner[1:], 16, 32)
185+
} else {
186+
code, err = strconv.ParseInt(inner, 10, 32)
187+
}
188+
if err == nil {
189+
return string(rune(code))
190+
}
191+
return match
192+
})
193+
194+
return result
195+
}
196+
197+
func init() {
198+
err := core.RegisterNodeFactory(stringDecodeDefinition, func(ctx any, parent core.NodeBaseInterface, parentId string, nodeDef map[string]any, validate bool) (core.NodeBaseInterface, []error) {
199+
return &StringDecode{}, nil
200+
})
201+
if err != nil {
202+
panic(err)
203+
}
204+
}

nodes/string-decode@v1.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
yaml-version: 3.0
2+
id: core/string-decode
3+
name: String Decode
4+
category: processing
5+
icon: tablerHash
6+
version: 1
7+
style:
8+
header:
9+
background: "#6e6e6e"
10+
body:
11+
background: "#423f3f"
12+
short_desc: Decode a string from various formats like Base64, Hex, UTF-16, UTF-32, or unescape HTML, URL, JSON, and XML sequences.
13+
addendum: |
14+
## Decoding Operations
15+
16+
This node takes an encoded string and decodes it back to UTF-8.
17+
18+
- **Printable Formats (Base64, Hex, etc.):**
19+
These operations decode a human-readable ASCII string back to its original form.
20+
21+
- **Raw Byte Formats (UTF-16, UTF-32):**
22+
These operations decode a string containing raw encoded bytes back to UTF-8.
23+
The input is expected to be produced by the `String Encode` node.
24+
25+
- **Unescape Formats (HTML, URL, JSON, XML):**
26+
These operations unescape special character sequences in the input string
27+
to restore the original characters.
28+
outputs:
29+
result:
30+
type: string
31+
index: 0
32+
desc: The resulting decoded string.
33+
inputs:
34+
input:
35+
name: Input
36+
type: string
37+
index: 0
38+
desc: The encoded string to decode.
39+
op:
40+
name: Decoding
41+
type: option
42+
index: 1
43+
default: base64
44+
desc: The decoding operation to apply.
45+
options:
46+
- name: From Base16 (Hex)
47+
value: hex
48+
- name: From Base32
49+
value: base32
50+
- name: From Base64
51+
value: base64
52+
- name: From Base64 (URL Safe)
53+
value: base64url
54+
- name: From UTF-16 LE
55+
value: utf16le
56+
- name: From UTF-16 BE
57+
value: utf16be
58+
- name: From UTF-32 LE
59+
value: utf32le
60+
- name: From UTF-32 BE
61+
value: utf32be
62+
- name: From HTML Unescape
63+
value: html
64+
- name: From URL Decode (Query)
65+
value: url
66+
- name: From URL Decode (Path)
67+
value: urlpath
68+
- name: From JSON Unescape
69+
value: json
70+
- name: From XML Unescape
71+
value: xml

nodes/string-encode@v1.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import (
55
"encoding/base32"
66
"encoding/base64"
77
"encoding/hex"
8+
"fmt"
9+
"html"
10+
"net/url"
11+
"strings"
812

913
"github.com/actionforge/actrun-cli/core"
1014
ni "github.com/actionforge/actrun-cli/node_interfaces"
@@ -76,13 +80,74 @@ func (n *StringEncode) OutputValueById(c *core.ExecutionState, outputId core.Out
7680
return nil, core.CreateErr(c, err, "failed to encode utf32be")
7781
}
7882
result = string(utf32Bytes) // Return raw bytes as a string
83+
84+
case "html":
85+
result = html.EscapeString(input)
86+
case "url":
87+
result = url.QueryEscape(input)
88+
case "urlpath":
89+
result = url.PathEscape(input)
90+
case "json":
91+
result = escapeJSON(input)
92+
case "xml":
93+
result = escapeXML(input)
7994
default:
8095
return nil, core.CreateErr(c, nil, "unknown operation '%s'", op)
8196
}
8297

8398
return result, nil
8499
}
85100

101+
func escapeJSON(s string) string {
102+
var b strings.Builder
103+
for _, r := range s {
104+
switch r {
105+
case '"':
106+
b.WriteString(`\"`)
107+
case '\\':
108+
b.WriteString(`\\`)
109+
case '\b':
110+
b.WriteString(`\b`)
111+
case '\f':
112+
b.WriteString(`\f`)
113+
case '\n':
114+
b.WriteString(`\n`)
115+
case '\r':
116+
b.WriteString(`\r`)
117+
case '\t':
118+
b.WriteString(`\t`)
119+
default:
120+
if r < 0x20 {
121+
b.WriteString(fmt.Sprintf(`\u%04x`, r))
122+
} else {
123+
b.WriteRune(r)
124+
}
125+
}
126+
}
127+
return b.String()
128+
}
129+
130+
func escapeXML(s string) string {
131+
var b strings.Builder
132+
for _, r := range s {
133+
switch r {
134+
case '<':
135+
b.WriteString("&lt;")
136+
case '>':
137+
b.WriteString("&gt;")
138+
case '&':
139+
b.WriteString("&amp;")
140+
case '\'':
141+
b.WriteString("&apos;")
142+
case '"':
143+
b.WriteString("&quot;")
144+
default:
145+
b.WriteRune(r)
146+
}
147+
}
148+
return b.String()
149+
}
150+
86151
func init() {
87152
err := core.RegisterNodeFactory(stringEncodeDefinition, func(ctx any, parent core.NodeBaseInterface, parentId string, nodeDef map[string]any, validate bool) (core.NodeBaseInterface, []error) {
88153
return &StringEncode{}, nil

0 commit comments

Comments
 (0)