summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-07-23 16:02:04 +0700
committerShulhan <ms@kilabit.info>2022-07-23 16:02:04 +0700
commitf6eeae8e1a4ae18c2e825db076b74531b4a343b6 (patch)
tree196e79f140f5031b4a44c412b8970eaf6496ebbc
parent95ccd72bc3d122847684ca7e3c213d909a10591c (diff)
downloadpakakeh.go-f6eeae8e1a4ae18c2e825db076b74531b4a343b6.tar.xz
lib/ini: support escaped double-quote and colon in tag subsection
A colon `:` is escaped using double backslash `\\`, for example `a:b\\:c:d` contains section `a`, subsection `b:c`, and variable `d`. A double quote `"` is escaped using triple backslash, for example `\\\"`.
-rw-r--r--lib/ini/common.go96
-rw-r--r--lib/ini/common_test.go73
-rw-r--r--lib/ini/doc.go15
-rw-r--r--lib/ini/ini.go22
-rw-r--r--lib/ini/ini_test.go9
-rw-r--r--lib/ini/tag_struct_field.go20
6 files changed, 198 insertions, 37 deletions
diff --git a/lib/ini/common.go b/lib/ini/common.go
index 0b8a9249..5a9d4813 100644
--- a/lib/ini/common.go
+++ b/lib/ini/common.go
@@ -43,3 +43,99 @@ func IsValueBoolTrue(v string) bool {
}
return false
}
+
+// parseTag parse the ini field tag as used in the struct's field.
+// This returned slice always have 4 string element: section, subsection, key,
+// and default value.
+func parseTag(in string) (tags []string) {
+ var (
+ sb strings.Builder
+ r rune
+ x int
+ foundSep bool
+ isEsc bool
+ )
+
+ tags = append(tags, ``, ``, ``, ``)
+
+ in = strings.TrimSpace(in)
+ if len(in) == 0 {
+ return tags
+ }
+
+ // Parse the section.
+ for x, r = range in {
+ if r == ':' {
+ foundSep = true
+ break
+ }
+ }
+ if !foundSep {
+ // If no ":" found, the tag is the section.
+ tags[0] = in
+ return tags
+ }
+
+ tags[0] = in[:x]
+ in = in[x+1:]
+
+ // Parse the subsection.
+ foundSep = false
+ sb.Reset()
+ for x, r = range in {
+ if r == '\\' {
+ if isEsc {
+ sb.WriteRune('\\')
+ isEsc = false
+ } else {
+ isEsc = true
+ }
+ continue
+ }
+ if r == '"' {
+ if isEsc {
+ sb.WriteRune(r)
+ isEsc = false
+ continue
+ }
+ break
+ }
+ if r == ':' {
+ if isEsc {
+ sb.WriteRune(r)
+ isEsc = false
+ continue
+ }
+ foundSep = true
+ break
+ }
+ sb.WriteRune(r)
+ }
+ tags[1] = sb.String()
+
+ if !foundSep {
+ return tags
+ }
+ in = in[x+1:]
+
+ // Parse variable name.
+ foundSep = false
+ for x, r = range in {
+ if r == ':' {
+ foundSep = true
+ break
+ }
+ }
+ if !foundSep {
+ tags[2] = in
+ return tags
+ }
+
+ tags[2] = in[:x]
+ in = in[x+1:]
+
+ // The rest is the default value.
+ tags[3] = in
+
+ return tags
+}
diff --git a/lib/ini/common_test.go b/lib/ini/common_test.go
index 09dd33ee..470b02b1 100644
--- a/lib/ini/common_test.go
+++ b/lib/ini/common_test.go
@@ -5,8 +5,10 @@
package ini
import (
+ "reflect"
"testing"
+ libreflect "github.com/shuLhan/share/lib/reflect"
"github.com/shuLhan/share/lib/test"
)
@@ -51,3 +53,74 @@ func TestIsValueBoolTrue(t *testing.T) {
test.Assert(t, "", c.exp, got)
}
}
+
+func TestParseTag(t *testing.T) {
+ type testCase struct {
+ in string
+ exp []string
+ }
+
+ var cases = []testCase{{
+ in: `sec`,
+ exp: []string{`sec`, ``, ``, ``},
+ }, {
+ in: `sec:sub`,
+ exp: []string{`sec`, `sub`, ``, ``},
+ }, {
+ in: `sec:sub:var`,
+ exp: []string{`sec`, `sub`, `var`, ``},
+ }, {
+ in: `sec:sub:var:def`,
+ exp: []string{`sec`, `sub`, `var`, `def`},
+ }, {
+ in: `sec:sub \"\:\\ name:var`,
+ exp: []string{`sec`, `sub ":\ name`, `var`, ``},
+ }}
+
+ var (
+ c testCase
+ got []string
+ )
+ for _, c = range cases {
+ got = parseTag(c.in)
+ test.Assert(t, c.in, c.exp, got)
+ }
+}
+
+func TestParseTag_fromStruct(t *testing.T) {
+ type ADT struct {
+ F1 int `ini:"a"`
+ F2 int `ini:"a:b"`
+ F3 int `ini:"a:b:c"`
+ F4 int `ini:"a:b:c:d"`
+ F5 int `ini:"a:b \\\"\\: c:d"`
+ }
+
+ var (
+ exp = [][]string{
+ {`a`, ``, ``, ``},
+ {`a`, `b`, ``, ``},
+ {`a`, `b`, `c`, ``},
+ {`a`, `b`, `c`, `d`},
+ {`a`, `b ": c`, `d`, ``},
+ }
+
+ adt ADT
+ vtype reflect.Type
+ field reflect.StructField
+ tag string
+ got []string
+ x int
+ )
+
+ vtype = reflect.TypeOf(adt)
+
+ for x = 0; x < vtype.NumField(); x++ {
+ field = vtype.Field(x)
+
+ tag, _, _ = libreflect.Tag(field, "ini")
+
+ got = parseTag(tag)
+ test.Assert(t, tag, exp[x], got)
+ }
+}
diff --git a/lib/ini/doc.go b/lib/ini/doc.go
index d7cf62e3..d6aad18d 100644
--- a/lib/ini/doc.go
+++ b/lib/ini/doc.go
@@ -121,8 +121,19 @@
// # Marshaling
//
// The container to be passed when marshaling must be struct type.
-// Each exported field in the struct with "ini" tags will be marshaled based
-// on the section, subsection, and key in the tag.
+// Each exported field in the struct with "ini" tag is marshaled based
+// on the section, subsection, and key in the tag's value.
+//
+// The "ini" tag syntax is,
+//
+// [SECTION] [':' SUBSECTION] [':' VAR]
+//
+// At least one of the section, subsection, or key should be defined.
+//
+// The subsection can contain colon and double quote.
+// A colon `:` is escaped using double backslash `\\`, for example `a:b\\:c:d`
+// contains section `a`, subsection `b:c`, and variable `d`.
+// A double quote `"` is escaped using triple backslash, for example `\\\"`.
//
// If the field type is slice of primitive, for example "[]int", it will be
// marshaled into multiple key with the same name.
diff --git a/lib/ini/ini.go b/lib/ini/ini.go
index f3edb073..c32a59f4 100644
--- a/lib/ini/ini.go
+++ b/lib/ini/ini.go
@@ -140,23 +140,12 @@ func (in *Ini) marshalStruct(
var sec, sub, key, value string
- tags = strings.Split(tag, fieldTagSeparator)
-
- switch len(tags) {
- case 0:
- if kind != reflect.Struct {
- continue
- }
- case 1:
- sec = tags[0]
- key = field.Name
- case 2:
- sec = tags[0]
- sub = tags[1]
+ tags = parseTag(tag)
+ sec = tags[0]
+ sub = tags[1]
+ if len(tags[2]) == 0 {
key = field.Name
- default:
- sec = tags[0]
- sub = tags[1]
+ } else {
key = tags[2]
}
if len(parentSec) > 0 {
@@ -165,7 +154,6 @@ func (in *Ini) marshalStruct(
if len(parentSub) > 0 {
sub = parentSub
}
-
key = strings.ToLower(key)
for kind == reflect.Ptr {
diff --git a/lib/ini/ini_test.go b/lib/ini/ini_test.go
index e6788718..f404f136 100644
--- a/lib/ini/ini_test.go
+++ b/lib/ini/ini_test.go
@@ -7,7 +7,6 @@ package ini
import (
"bytes"
"fmt"
- "strings"
"testing"
"time"
@@ -559,12 +558,8 @@ func TestIni_Get(t *testing.T) {
continue
}
- tags = strings.Split(string(key), fieldTagSeparator)
- if len(tags) >= 4 {
- def = tags[3]
- } else {
- def = ""
- }
+ tags = parseTag(string(key))
+ def = tags[3]
got, ok = cfg.Get(tags[0], tags[1], tags[2], def)
got = fmt.Sprintf("%t %s.", ok, got)
diff --git a/lib/ini/tag_struct_field.go b/lib/ini/tag_struct_field.go
index ab739b63..7d99c9e4 100644
--- a/lib/ini/tag_struct_field.go
+++ b/lib/ini/tag_struct_field.go
@@ -17,6 +17,10 @@ type tagStructField map[string]*structField
// subsection, and/or key along with their reflect type and value into
// structField.
func unpackTagStructField(rtype reflect.Type, rval reflect.Value) (out tagStructField) {
+ var (
+ tags []string
+ )
+
numField := rtype.NumField()
if numField == 0 {
return nil
@@ -70,20 +74,14 @@ func unpackTagStructField(rtype reflect.Type, rval reflect.Value) (out tagStruct
sfield.layout = time.RFC3339
}
- tags := strings.Split(tag, fieldTagSeparator)
+ tags = parseTag(tag)
+ sfield.sec = tags[0]
+ sfield.sub = tags[1]
+ sfield.key = strings.ToLower(tags[2])
- switch len(tags) {
- case 1:
- sfield.sec = tags[0]
- sfield.key = sfield.fname
- case 2:
+ if len(sfield.key) == 0 {
sfield.sec = tags[0]
- sfield.sub = tags[1]
sfield.key = sfield.fname
- default:
- sfield.sec = tags[0]
- sfield.sub = tags[1]
- sfield.key = strings.ToLower(tags[2])
}
out[tag] = sfield