ini: support Python multi-line values (#145)

* [GH-144] Support Python multi-line values
This commit is contained in:
Anton Antonov
2018-04-20 10:53:26 -04:00
committed by 无闻
parent ace140f734
commit ecf73439ed
3 changed files with 1013 additions and 157 deletions
+5
View File
@@ -140,6 +140,11 @@ type LoadOptions struct {
// AllowNestedValues indicates whether to allow AWS-like nested values.
// Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values
AllowNestedValues bool
// AllowPythonMultilineValues indicates whether to allow Python-like multi-line values.
// Docs: https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
// Relevant quote: Values can also span multiple lines, as long as they are indented deeper
// than the first line of the value.
AllowPythonMultilineValues bool
// UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format
// when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value"
UnescapeValueDoubleQuotes bool
+788 -13
View File
@@ -87,12 +87,133 @@ NAME = Unknwon
So(err, ShouldNotBeNil)
})
})
Convey("Cant't parse small python-compatible INI files", t, func() {
f, err := ini.Load([]byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
foo
bar
foobar
barfoo
-----END RSA PRIVATE KEY-----
`))
So(err, ShouldNotBeNil)
So(f, ShouldBeNil)
So(err.Error(), ShouldEqual, "key-value delimiter not found: foo\n")
})
Convey("Cant't parse big python-compatible INI files", t, func() {
f, err := ini.Load([]byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
1foo
2bar
3foobar
4barfoo
5foo
6bar
7foobar
8barfoo
9foo
10bar
11foobar
12barfoo
13foo
14bar
15foobar
16barfoo
17foo
18bar
19foobar
20barfoo
21foo
22bar
23foobar
24barfoo
25foo
26bar
27foobar
28barfoo
29foo
30bar
31foobar
32barfoo
33foo
34bar
35foobar
36barfoo
37foo
38bar
39foobar
40barfoo
41foo
42bar
43foobar
44barfoo
45foo
46bar
47foobar
48barfoo
49foo
50bar
51foobar
52barfoo
53foo
54bar
55foobar
56barfoo
57foo
58bar
59foobar
60barfoo
61foo
62bar
63foobar
64barfoo
65foo
66bar
67foobar
68barfoo
69foo
70bar
71foobar
72barfoo
73foo
74bar
75foobar
76barfoo
77foo
78bar
79foobar
80barfoo
81foo
82bar
83foobar
84barfoo
85foo
86bar
87foobar
88barfoo
89foo
90bar
91foobar
92barfoo
93foo
94bar
95foobar
96barfoo
-----END RSA PRIVATE KEY-----
`))
So(err, ShouldNotBeNil)
So(f, ShouldBeNil)
So(err.Error(), ShouldEqual, "key-value delimiter not found: 1foo\n")
})
}
func TestLoadSources(t *testing.T) {
Convey("Load from data sources with options", t, func() {
Convey("Ignore nonexistent files", func() {
f, err := ini.LooseLoad(_NOT_FOUND_CONF, _MINIMAL_CONF)
func TestLooseLoad(t *testing.T) {
Convey("Load from data sources with option `Loose` true", t, func() {
f, err := ini.LoadSources(ini.LoadOptions{Loose: true}, _NOT_FOUND_CONF, _MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
@@ -101,8 +222,10 @@ func TestLoadSources(t *testing.T) {
So(err, ShouldNotBeNil)
})
})
}
Convey("Insensitive to section and key names", func() {
func TestInsensitiveLoad(t *testing.T) {
Convey("Insensitive to section and key names", t, func() {
f, err := ini.InsensitiveLoad(_MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
@@ -127,9 +250,51 @@ e-mail = u@gogs.io
So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty)
})
})
}
func TestLoadSources(t *testing.T) {
Convey("Load from data sources with options", t, func() {
Convey("with true `AllowPythonMultilineValues`", func() {
Convey("Ignore nonexistent files", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true, Loose: true}, _NOT_FOUND_CONF, _MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
Convey("Inverse case", func() {
_, err = ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, _NOT_FOUND_CONF)
So(err, ShouldNotBeNil)
})
})
Convey("Insensitive to section and key names", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true, Insensitive: true}, _MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io")
Convey("Write out", func() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, `[author]
e-mail = u@gogs.io
`)
})
Convey("Inverse case", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, _MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty)
})
})
Convey("Ignore continuation lines", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
IgnoreContinuation: true,
}, []byte(`
key1=a\b\
@@ -143,7 +308,7 @@ key3=value`))
So(f.Section("").Key("key3").String(), ShouldEqual, "value")
Convey("Inverse case", func() {
f, err := ini.Load([]byte(`
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
key1=a\b\
key2=c\d\`))
So(err, ShouldBeNil)
@@ -155,6 +320,7 @@ key2=c\d\`))
Convey("Ignore inline comments", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
IgnoreInlineComment: true,
}, []byte(`
key1=value ;comment
@@ -166,7 +332,7 @@ key2=value2 #comment2`))
So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`)
Convey("Inverse case", func() {
f, err := ini.Load([]byte(`
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
key1=value ;comment
key2=value2 #comment2`))
So(err, ShouldBeNil)
@@ -181,6 +347,7 @@ key2=value2 #comment2`))
Convey("Allow boolean type keys", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
AllowBooleanKeys: true,
}, []byte(`
key1=hello
@@ -203,7 +370,7 @@ key3
})
Convey("Inverse case", func() {
_, err := ini.Load([]byte(`
_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
key1=hello
#key2
key3`))
@@ -212,7 +379,7 @@ key3`))
})
Convey("Allow shadow keys", func() {
f, err := ini.ShadowLoad([]byte(`
f, err := ini.LoadSources(ini.LoadOptions{AllowShadows: true, AllowPythonMultilineValues: true}, []byte(`
[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git
@@ -240,7 +407,7 @@ fetch = +refs/heads/*:refs/remotes/origin/*
})
Convey("Inverse case", func() {
f, err := ini.Load([]byte(`
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git`))
@@ -253,6 +420,7 @@ url = https://github.com/Antergone/test2.git`))
Convey("Unescape double quotes inside value", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
UnescapeValueDoubleQuotes: true,
}, []byte(`
create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
@@ -262,7 +430,7 @@ create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 <a href="%s">%s</a>`)
Convey("Inverse case", func() {
f, err := ini.Load([]byte(`
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
@@ -273,6 +441,7 @@ create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
Convey("Unescape comment symbols inside value", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, []byte(`
@@ -284,8 +453,238 @@ key = test value <span style="color: %s\; background: %s">more text</span>
So(f.Section("").Key("key").String(), ShouldEqual, `test value <span style="color: %s; background: %s">more text</span>`)
})
Convey("Allow unparseable sections", func() {
Convey("Can parse small python-compatible INI files", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
Insensitive: true,
UnparseableSections: []string{"core_lesson", "comments"},
}, []byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
foo
bar
foobar
barfoo
-----END RSA PRIVATE KEY-----
`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("long").Key("long_rsa_private_key").String(), ShouldEqual, "-----BEGIN RSA PRIVATE KEY-----\nfoo\nbar\nfoobar\nbarfoo\n-----END RSA PRIVATE KEY-----")
})
Convey("Can parse big python-compatible INI files", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
Insensitive: true,
UnparseableSections: []string{"core_lesson", "comments"},
}, []byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
1foo
2bar
3foobar
4barfoo
5foo
6bar
7foobar
8barfoo
9foo
10bar
11foobar
12barfoo
13foo
14bar
15foobar
16barfoo
17foo
18bar
19foobar
20barfoo
21foo
22bar
23foobar
24barfoo
25foo
26bar
27foobar
28barfoo
29foo
30bar
31foobar
32barfoo
33foo
34bar
35foobar
36barfoo
37foo
38bar
39foobar
40barfoo
41foo
42bar
43foobar
44barfoo
45foo
46bar
47foobar
48barfoo
49foo
50bar
51foobar
52barfoo
53foo
54bar
55foobar
56barfoo
57foo
58bar
59foobar
60barfoo
61foo
62bar
63foobar
64barfoo
65foo
66bar
67foobar
68barfoo
69foo
70bar
71foobar
72barfoo
73foo
74bar
75foobar
76barfoo
77foo
78bar
79foobar
80barfoo
81foo
82bar
83foobar
84barfoo
85foo
86bar
87foobar
88barfoo
89foo
90bar
91foobar
92barfoo
93foo
94bar
95foobar
96barfoo
-----END RSA PRIVATE KEY-----
`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("long").Key("long_rsa_private_key").String(), ShouldEqual, `-----BEGIN RSA PRIVATE KEY-----
1foo
2bar
3foobar
4barfoo
5foo
6bar
7foobar
8barfoo
9foo
10bar
11foobar
12barfoo
13foo
14bar
15foobar
16barfoo
17foo
18bar
19foobar
20barfoo
21foo
22bar
23foobar
24barfoo
25foo
26bar
27foobar
28barfoo
29foo
30bar
31foobar
32barfoo
33foo
34bar
35foobar
36barfoo
37foo
38bar
39foobar
40barfoo
41foo
42bar
43foobar
44barfoo
45foo
46bar
47foobar
48barfoo
49foo
50bar
51foobar
52barfoo
53foo
54bar
55foobar
56barfoo
57foo
58bar
59foobar
60barfoo
61foo
62bar
63foobar
64barfoo
65foo
66bar
67foobar
68barfoo
69foo
70bar
71foobar
72barfoo
73foo
74bar
75foobar
76barfoo
77foo
78bar
79foobar
80barfoo
81foo
82bar
83foobar
84barfoo
85foo
86bar
87foobar
88barfoo
89foo
90bar
91foobar
92barfoo
93foo
94bar
95foobar
96barfoo
-----END RSA PRIVATE KEY-----`)
})
Convey("Allow unparsable sections", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: true,
Insensitive: true,
UnparseableSections: []string{"core_lesson", "comments"},
}, []byte(`
@@ -328,7 +727,7 @@ my lesson state data – 1111111111111111111000000000000000001110000
})
Convey("Inverse case", func() {
_, err := ini.Load([]byte(`
_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: true}, []byte(`
[CORE_LESSON]
my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data`))
@@ -336,4 +735,380 @@ my lesson state data – 1111111111111111111000000000000000001110000
})
})
})
Convey("with false `AllowPythonMultilineValues`", func() {
Convey("Ignore nonexistent files", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false, Loose: true}, _NOT_FOUND_CONF, _MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
Convey("Inverse case", func() {
_, err = ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, _NOT_FOUND_CONF)
So(err, ShouldNotBeNil)
})
})
Convey("Insensitive to section and key names", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false, Insensitive: true}, _MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("Author").Key("e-mail").String(), ShouldEqual, "u@gogs.io")
Convey("Write out", func() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, `[author]
e-mail = u@gogs.io
`)
})
Convey("Inverse case", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, _MINIMAL_CONF)
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("Author").Key("e-mail").String(), ShouldBeEmpty)
})
})
Convey("Ignore continuation lines", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: false,
IgnoreContinuation: true,
}, []byte(`
key1=a\b\
key2=c\d\
key3=value`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("key1").String(), ShouldEqual, `a\b\`)
So(f.Section("").Key("key2").String(), ShouldEqual, `c\d\`)
So(f.Section("").Key("key3").String(), ShouldEqual, "value")
Convey("Inverse case", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
key1=a\b\
key2=c\d\`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("key1").String(), ShouldEqual, `a\bkey2=c\d`)
})
})
Convey("Ignore inline comments", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: false,
IgnoreInlineComment: true,
}, []byte(`
key1=value ;comment
key2=value2 #comment2`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("key1").String(), ShouldEqual, `value ;comment`)
So(f.Section("").Key("key2").String(), ShouldEqual, `value2 #comment2`)
Convey("Inverse case", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
key1=value ;comment
key2=value2 #comment2`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("key1").String(), ShouldEqual, `value`)
So(f.Section("").Key("key1").Comment, ShouldEqual, `;comment`)
So(f.Section("").Key("key2").String(), ShouldEqual, `value2`)
So(f.Section("").Key("key2").Comment, ShouldEqual, `#comment2`)
})
})
Convey("Allow boolean type keys", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: false,
AllowBooleanKeys: true,
}, []byte(`
key1=hello
#key2
key3`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").KeyStrings(), ShouldResemble, []string{"key1", "key3"})
So(f.Section("").Key("key3").MustBool(false), ShouldBeTrue)
Convey("Write out", func() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, `key1 = hello
# key2
key3
`)
})
Convey("Inverse case", func() {
_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
key1=hello
#key2
key3`))
So(err, ShouldNotBeNil)
})
})
Convey("Allow shadow keys", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false, AllowShadows: true}, []byte(`
[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git
fetch = +refs/heads/*:refs/remotes/origin/*`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test1.git")
So(f.Section(`remote "origin"`).Key("url").ValueWithShadows(), ShouldResemble, []string{
"https://github.com/Antergone/test1.git",
"https://github.com/Antergone/test2.git",
})
So(f.Section(`remote "origin"`).Key("fetch").String(), ShouldEqual, "+refs/heads/*:refs/remotes/origin/*")
Convey("Write out", func() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, `[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`)
})
Convey("Inverse case", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section(`remote "origin"`).Key("url").String(), ShouldEqual, "https://github.com/Antergone/test2.git")
})
})
Convey("Unescape double quotes inside value", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: false,
UnescapeValueDoubleQuotes: true,
}, []byte(`
create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("create_repo").String(), ShouldEqual, `创建了仓库 <a href="%s">%s</a>`)
Convey("Inverse case", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
create_repo="创建了仓库 <a href=\"%s\">%s</a>"`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("create_repo").String(), ShouldEqual, `"创建了仓库 <a href=\"%s\">%s</a>"`)
})
})
Convey("Unescape comment symbols inside value", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: false,
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, []byte(`
key = test value <span style="color: %s\; background: %s">more text</span>
`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("key").String(), ShouldEqual, `test value <span style="color: %s; background: %s">more text</span>`)
})
Convey("Cant't parse small python-compatible INI files", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
foo
bar
foobar
barfoo
-----END RSA PRIVATE KEY-----
`))
So(err, ShouldNotBeNil)
So(f, ShouldBeNil)
So(err.Error(), ShouldEqual, "key-value delimiter not found: foo\n")
})
Convey("Cant't parse big python-compatible INI files", func() {
f, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
1foo
2bar
3foobar
4barfoo
5foo
6bar
7foobar
8barfoo
9foo
10bar
11foobar
12barfoo
13foo
14bar
15foobar
16barfoo
17foo
18bar
19foobar
20barfoo
21foo
22bar
23foobar
24barfoo
25foo
26bar
27foobar
28barfoo
29foo
30bar
31foobar
32barfoo
33foo
34bar
35foobar
36barfoo
37foo
38bar
39foobar
40barfoo
41foo
42bar
43foobar
44barfoo
45foo
46bar
47foobar
48barfoo
49foo
50bar
51foobar
52barfoo
53foo
54bar
55foobar
56barfoo
57foo
58bar
59foobar
60barfoo
61foo
62bar
63foobar
64barfoo
65foo
66bar
67foobar
68barfoo
69foo
70bar
71foobar
72barfoo
73foo
74bar
75foobar
76barfoo
77foo
78bar
79foobar
80barfoo
81foo
82bar
83foobar
84barfoo
85foo
86bar
87foobar
88barfoo
89foo
90bar
91foobar
92barfoo
93foo
94bar
95foobar
96barfoo
-----END RSA PRIVATE KEY-----
`))
So(err, ShouldNotBeNil)
So(f, ShouldBeNil)
So(err.Error(), ShouldEqual, "key-value delimiter not found: 1foo\n")
})
Convey("Allow unparsable sections", func() {
f, err := ini.LoadSources(ini.LoadOptions{
AllowPythonMultilineValues: false,
Insensitive: true,
UnparseableSections: []string{"core_lesson", "comments"},
}, []byte(`
Lesson_Location = 87
Lesson_Status = C
Score = 3
Time = 00:02:30
[CORE_LESSON]
my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data
[COMMENTS]
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("").Key("score").String(), ShouldEqual, "3")
So(f.Section("").Body(), ShouldBeEmpty)
So(f.Section("core_lesson").Body(), ShouldEqual, `my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data`)
So(f.Section("comments").Body(), ShouldEqual, `<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`)
Convey("Write out", func() {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, `lesson_location = 87
lesson_status = C
score = 3
time = 00:02:30
[core_lesson]
my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data
[comments]
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>
`)
})
Convey("Inverse case", func() {
_, err := ini.LoadSources(ini.LoadOptions{AllowPythonMultilineValues: false}, []byte(`
[CORE_LESSON]
my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data`))
So(err, ShouldNotBeNil)
})
})
})
})
}
+80 -4
View File
@@ -19,11 +19,14 @@ import (
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"unicode"
)
var pythonMultiline = regexp.MustCompile("^(\\s+)([^\n]+)")
type tokenType int
const (
@@ -194,7 +197,8 @@ func hasSurroundedQuote(in string, quote byte) bool {
}
func (p *parser) readValue(in []byte,
ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols bool) (string, error) {
parserBufferSize int,
ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols, allowPythonMultilines bool) (string, error) {
line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
if len(line) == 0 {
@@ -224,11 +228,13 @@ func (p *parser) readValue(in []byte,
return line[startIdx : pos+startIdx], nil
}
lastChar := line[len(line)-1]
// Won't be able to reach here if value only contains whitespace
line = strings.TrimSpace(line)
trimmedLastChar := line[len(line)-1]
// Check continuation lines when desired
if !ignoreContinuation && line[len(line)-1] == '\\' {
if !ignoreContinuation && trimmedLastChar == '\\' {
return p.readContinuationLines(line[:len(line)-1])
}
@@ -252,7 +258,50 @@ func (p *parser) readValue(in []byte,
if strings.Contains(line, `\#`) {
line = strings.Replace(line, `\#`, "#", -1)
}
} else if allowPythonMultilines && lastChar == '\n' {
parserBufferPeekResult, _ := p.buf.Peek(parserBufferSize)
peekBuffer := bytes.NewBuffer(parserBufferPeekResult)
identSize := -1
val := line
for {
peekData, peekErr := peekBuffer.ReadBytes('\n')
if peekErr != nil {
if peekErr == io.EOF {
return val, nil
}
return "", peekErr
}
peekMatches := pythonMultiline.FindStringSubmatch(string(peekData))
if len(peekMatches) != 3 {
return val, nil
}
currentIdentSize := len(peekMatches[1])
// NOTE: Return if not a python-ini multi-line value.
if currentIdentSize < 0 {
return val, nil
}
identSize = currentIdentSize
// NOTE: Just advance the parser reader (buffer) in-sync with the peek buffer.
_, err := p.readUntil('\n')
if err != nil {
return "", err
}
val += fmt.Sprintf("\n%s", peekMatches[2])
}
// NOTE: If it was a Python multi-line value,
// return the appended value.
if identSize > 0 {
return val, nil
}
}
return line, nil
}
@@ -276,6 +325,29 @@ func (f *File) parse(reader io.Reader) (err error) {
var line []byte
var inUnparseableSection bool
// NOTE: Iterate and increase `currentPeekSize` until
// the size of the parser buffer is found.
// TODO: When Golang 1.10 is the lowest version supported,
// replace with `parserBufferSize := p.buf.Size()`.
parserBufferSize := 0
// NOTE: Peek 1kb at a time.
currentPeekSize := 1024
if f.options.AllowPythonMultilineValues {
for {
peekBytes, _ := p.buf.Peek(currentPeekSize)
peekBytesLength := len(peekBytes)
if parserBufferSize >= peekBytesLength {
break
}
currentPeekSize *= 2
parserBufferSize = peekBytesLength
}
}
for !p.isEOF {
line, err = p.readUntil('\n')
if err != nil {
@@ -352,10 +424,12 @@ func (f *File) parse(reader io.Reader) (err error) {
// Treat as boolean key when desired, and whole line is key name.
if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
kname, err := p.readValue(line,
parserBufferSize,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols)
f.options.UnescapeValueCommentSymbols,
f.options.AllowPythonMultilineValues)
if err != nil {
return err
}
@@ -379,10 +453,12 @@ func (f *File) parse(reader io.Reader) (err error) {
}
value, err := p.readValue(line[offset:],
parserBufferSize,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols)
f.options.UnescapeValueCommentSymbols,
f.options.AllowPythonMultilineValues)
if err != nil {
return err
}