pax_global_header 0000666 0000000 0000000 00000000064 14274172402 0014515 g ustar 00root root 0000000 0000000 52 comment=b2f570e5b5b844226bbefe6fb521d891f529a951
ini-1.67.0/ 0000775 0000000 0000000 00000000000 14274172402 0012367 5 ustar 00root root 0000000 0000000 ini-1.67.0/.editorconfig 0000664 0000000 0000000 00000000267 14274172402 0015051 0 ustar 00root root 0000000 0000000 # http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*_test.go]
trim_trailing_whitespace = false
ini-1.67.0/.github/ 0000775 0000000 0000000 00000000000 14274172402 0013727 5 ustar 00root root 0000000 0000000 ini-1.67.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14274172402 0016112 5 ustar 00root root 0000000 0000000 ini-1.67.0/.github/ISSUE_TEMPLATE/bug_report.yml 0000664 0000000 0000000 00000003415 14274172402 0021010 0 ustar 00root root 0000000 0000000 name: Bug report
description: File a bug report to help us improve
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- Before you file an issue read the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
- Check to make sure someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
- type: input
attributes:
label: Version
description: Please specify the exact Go module version you're reporting for.
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To reproduce
description: A code snippet to reproduce the problem described above.
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: |
Links? References? Suggestions? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: checkboxes
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct)
options:
- label: I agree to follow this project's Code of Conduct
required: true
ini-1.67.0/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000034 14274172402 0020077 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false
ini-1.67.0/.github/ISSUE_TEMPLATE/documentation.yml 0000664 0000000 0000000 00000002203 14274172402 0021503 0 ustar 00root root 0000000 0000000 name: Improve documentation
description: Suggest an idea or a patch for documentation
labels: ["documentation"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this form!
- Before you file an issue read the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
- Check to make sure someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
- type: textarea
attributes:
label: What needs to be improved? Please describe
description: A clear and concise description of what is wrong or missing.
validations:
required: true
- type: textarea
attributes:
label: Why do you think it is important?
description: A clear and concise explanation of the rationale.
validations:
required: true
- type: checkboxes
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct)
options:
- label: I agree to follow this project's Code of Conduct
required: true
ini-1.67.0/.github/ISSUE_TEMPLATE/feature_request.yml 0000664 0000000 0000000 00000003324 14274172402 0022042 0 ustar 00root root 0000000 0000000 name: Feature request
description: Suggest an idea for this project
labels: ["feature"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this form!
- Before you file an issue read the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
- Check to make sure someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
- type: textarea
attributes:
label: Describe the feature
description: A clear and concise description of what the problem is, e.g. I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: |
Links? References? Suggestions? Anything that will give us more context about the feature you are requesting!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: checkboxes
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://go.dev/conduct)
options:
- label: I agree to follow this project's Code of Conduct
required: true
ini-1.67.0/.github/PULL_REQUEST_TEMPLATE.md 0000664 0000000 0000000 00000001005 14274172402 0017524 0 ustar 00root root 0000000 0000000 ### Describe the pull request
A clear and concise description of what the pull request is about, i.e. what problem should be fixed?
Link to the issue:
### Checklist
- [ ] I agree to follow the [Code of Conduct](https://go.dev/conduct) by submitting this pull request.
- [ ] I have read and acknowledge the [Contributing guide](https://github.com/go-ini/ini/blob/main/.github/contributing.md).
- [ ] I have added test cases to cover the new code.
ini-1.67.0/.github/contributing.md 0000664 0000000 0000000 00000007601 14274172402 0016764 0 ustar 00root root 0000000 0000000 # Welcome to go-ini contributing guide
Thank you for investing your time in contributing to our projects!
Read our [Code of Conduct](https://go.dev/conduct) to keep our community approachable and respectable.
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
Use the table of contents icon
on the top left corner of this document to get to a specific section of this guide quickly.
## New contributor guide
To get an overview of the project, read the [README](/README.md). Here are some resources to help you get started with open source contributions:
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow)
- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests)
In addition to the general guides with open source contributions, you would also need to:
- Have basic knowledge about INI configuration format and programming in [Go](https://go.dev/).
- Have a working local development setup with a reasonable good IDE or editor like [Visual Studio Code](https://code.visualstudio.com/docs/languages/go), [GoLand](https://www.jetbrains.com/go/) or [Vim](https://github.com/fatih/vim-go).
## Issues
### Create a new issue
- [Check to make sure](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments) someone hasn't already opened a similar [issue](https://github.com/go-ini/ini/issues).
- If a similar issue doesn't exist, open a new issue using a relevant [issue form](https://github.com/go-ini/ini/issues/new/choose).
### Pick up an issue to solve
- Scan through our [existing issues](https://github.com/go-ini/ini/issues) to find one that interests you.
- The [good first issue](https://github.com/go-ini/ini/labels/good%20first%20issue) is a good place to start exploring issues that are well-groomed for newcomers.
- Do not hesitate to ask for more details or clarifying questions on the issue!
- Communicate on the issue you are intended to pick up _before_ starting working on it.
- Every issue that gets picked up will have an expected timeline for the implementation, the issue may be reassigned after the expected timeline. Please be responsible and proactive on the communication 🙇â€â™‚ï¸
## Pull requests
When you're finished with the changes, create a pull request, or a series of pull requests if necessary.
Contributing to another codebase is not as simple as code changes, it is also about contributing influence to the design. Therefore, we kindly ask you that:
- Please acknowledge that no pull request is guaranteed to be merged.
- Please always do a self-review before requesting reviews from others.
- Please expect code review to be strict and may have multiple rounds.
- Please make self-contained incremental changes, pull requests with huge diff may be rejected for review.
- Please use English in code comments and docstring.
- Please do not force push unless absolutely necessary. Force pushes make review much harder in multiple rounds, and we use [Squash and merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits) so you don't need to worry about messy commits and just focus on the changes.
## Your PR is merged!
Congratulations 🎉🎉 Thanks again for taking the effort to have this journey with us 🌟
ini-1.67.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14274172402 0015764 5 ustar 00root root 0000000 0000000 ini-1.67.0/.github/workflows/go.yml 0000664 0000000 0000000 00000003074 14274172402 0017120 0 ustar 00root root 0000000 0000000 name: Go
on:
push:
branches: [ main ]
paths:
- '**.go'
- 'go.mod'
- '.golangci.yml'
- '.github/workflows/go.yml'
pull_request:
paths:
- '**.go'
- 'go.mod'
- '.golangci.yml'
- '.github/workflows/go.yml'
env:
GOPROXY: "https://proxy.golang.org"
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Init Go Modules
run: |
go mod init github.com/go-ini/ini
go mod tidy
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
args: --timeout=30m
skip-pkg-cache: true # Wrokaround of the "tar" problem: https://github.com/golangci/golangci-lint-action/issues/244
test:
name: Test
strategy:
matrix:
go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x ]
platform: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests with coverage
run: |
go mod init github.com/go-ini/ini
go mod tidy
go test -v -race -coverprofile=coverage -covermode=atomic ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v1.5.0
with:
file: ./coverage
flags: unittests
ini-1.67.0/.github/workflows/lsif.yml 0000664 0000000 0000000 00000001470 14274172402 0017446 0 ustar 00root root 0000000 0000000 name: LSIF
on:
push:
paths:
- '**.go'
- 'go.mod'
- '.github/workflows/lsif.yml'
env:
GOPROXY: "https://proxy.golang.org"
jobs:
lsif-go:
if: github.repository == 'go-ini/ini'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Generate LSIF data
uses: sourcegraph/lsif-go-action@master
- name: Upload LSIF data to sourcegraph.com
continue-on-error: true
uses: docker://sourcegraph/src-cli:latest
with:
args: lsif upload -github-token=${{ secrets.GITHUB_TOKEN }}
- name: Upload LSIF data to cs.unknwon.dev
continue-on-error: true
uses: docker://sourcegraph/src-cli:latest
with:
args: -endpoint=https://cs.unknwon.dev lsif upload -github-token=${{ secrets.GITHUB_TOKEN }}
ini-1.67.0/.gitignore 0000664 0000000 0000000 00000000163 14274172402 0014357 0 ustar 00root root 0000000 0000000 testdata/conf_out.ini
ini.sublime-project
ini.sublime-workspace
testdata/conf_reflect.ini
.idea
/.vscode
.DS_Store
ini-1.67.0/.golangci.yml 0000664 0000000 0000000 00000000724 14274172402 0014756 0 ustar 00root root 0000000 0000000 linters-settings:
staticcheck:
checks: [
"all",
"-SA1019" # There are valid use cases of strings.Title
]
nakedret:
max-func-lines: 0 # Disallow any unnamed return statement
linters:
enable:
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
- nakedret
- gofmt
- rowserrcheck
- unconvert
- goimports
- unparam
ini-1.67.0/LICENSE 0000664 0000000 0000000 00000024015 14274172402 0013376 0 ustar 00root root 0000000 0000000 Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright 2014 Unknwon
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
ini-1.67.0/Makefile 0000664 0000000 0000000 00000000357 14274172402 0014034 0 ustar 00root root 0000000 0000000 .PHONY: build test bench vet coverage
build: vet bench
test:
go test -v -cover -race
bench:
go test -v -cover -test.bench=. -test.benchmem
vet:
go vet
coverage:
go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out
ini-1.67.0/README.md 0000664 0000000 0000000 00000003253 14274172402 0013651 0 ustar 00root root 0000000 0000000 # INI
[](https://github.com/go-ini/ini/actions?query=branch%3Amain)
[](https://codecov.io/gh/go-ini/ini)
[](https://pkg.go.dev/github.com/go-ini/ini?tab=doc)
[](https://sourcegraph.com/github.com/go-ini/ini)

Package ini provides INI file read and write functionality in Go.
## Features
- Load from multiple data sources(file, `[]byte`, `io.Reader` and `io.ReadCloser`) with overwrites.
- Read with recursion values.
- Read with parent-child sections.
- Read with auto-increment key names.
- Read with multiple-line values.
- Read with tons of helper methods.
- Read and convert values to Go types.
- Read and **WRITE** comments of sections and keys.
- Manipulate sections, keys and comments with ease.
- Keep sections and keys in order as you parse and save.
## Installation
The minimum requirement of Go is **1.13**.
```sh
$ go get gopkg.in/ini.v1
```
Please add `-u` flag to update in the future.
## Getting Help
- [Getting Started](https://ini.unknwon.io/docs/intro/getting_started)
- [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
- ä¸å›½å¤§é™†é•œåƒï¼šhttps://ini.unknwon.cn
## License
This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text.
ini-1.67.0/bench_test.go 0000664 0000000 0000000 00000005036 14274172402 0015040 0 ustar 00root root 0000000 0000000 // Copyright 2017 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"testing"
)
func newTestFile(block bool) *File {
c, _ := Load([]byte(confData))
c.BlockMode = block
return c
}
func Benchmark_Key_Value(b *testing.B) {
c := newTestFile(true)
for i := 0; i < b.N; i++ {
c.Section("").Key("NAME").Value()
}
}
func Benchmark_Key_Value_NonBlock(b *testing.B) {
c := newTestFile(false)
for i := 0; i < b.N; i++ {
c.Section("").Key("NAME").Value()
}
}
func Benchmark_Key_Value_ViaSection(b *testing.B) {
c := newTestFile(true)
sec := c.Section("")
for i := 0; i < b.N; i++ {
sec.Key("NAME").Value()
}
}
func Benchmark_Key_Value_ViaSection_NonBlock(b *testing.B) {
c := newTestFile(false)
sec := c.Section("")
for i := 0; i < b.N; i++ {
sec.Key("NAME").Value()
}
}
func Benchmark_Key_Value_Direct(b *testing.B) {
c := newTestFile(true)
key := c.Section("").Key("NAME")
for i := 0; i < b.N; i++ {
key.Value()
}
}
func Benchmark_Key_Value_Direct_NonBlock(b *testing.B) {
c := newTestFile(false)
key := c.Section("").Key("NAME")
for i := 0; i < b.N; i++ {
key.Value()
}
}
func Benchmark_Key_String(b *testing.B) {
c := newTestFile(true)
for i := 0; i < b.N; i++ {
_ = c.Section("").Key("NAME").String()
}
}
func Benchmark_Key_String_NonBlock(b *testing.B) {
c := newTestFile(false)
for i := 0; i < b.N; i++ {
_ = c.Section("").Key("NAME").String()
}
}
func Benchmark_Key_String_ViaSection(b *testing.B) {
c := newTestFile(true)
sec := c.Section("")
for i := 0; i < b.N; i++ {
_ = sec.Key("NAME").String()
}
}
func Benchmark_Key_String_ViaSection_NonBlock(b *testing.B) {
c := newTestFile(false)
sec := c.Section("")
for i := 0; i < b.N; i++ {
_ = sec.Key("NAME").String()
}
}
func Benchmark_Key_SetValue(b *testing.B) {
c := newTestFile(true)
for i := 0; i < b.N; i++ {
c.Section("").Key("NAME").SetValue("10")
}
}
func Benchmark_Key_SetValue_VisSection(b *testing.B) {
c := newTestFile(true)
sec := c.Section("")
for i := 0; i < b.N; i++ {
sec.Key("NAME").SetValue("10")
}
}
ini-1.67.0/codecov.yml 0000664 0000000 0000000 00000000365 14274172402 0014540 0 ustar 00root root 0000000 0000000 coverage:
range: "60...95"
status:
project:
default:
threshold: 1%
informational: true
patch:
defualt:
only_pulls: true
informational: true
comment:
layout: 'diff'
github_checks: false
ini-1.67.0/data_source.go 0000664 0000000 0000000 00000003654 14274172402 0015217 0 ustar 00root root 0000000 0000000 // Copyright 2019 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
)
var (
_ dataSource = (*sourceFile)(nil)
_ dataSource = (*sourceData)(nil)
_ dataSource = (*sourceReadCloser)(nil)
)
// dataSource is an interface that returns object which can be read and closed.
type dataSource interface {
ReadCloser() (io.ReadCloser, error)
}
// sourceFile represents an object that contains content on the local file system.
type sourceFile struct {
name string
}
func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) {
return os.Open(s.name)
}
// sourceData represents an object that contains content in memory.
type sourceData struct {
data []byte
}
func (s *sourceData) ReadCloser() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(s.data)), nil
}
// sourceReadCloser represents an input stream with Close method.
type sourceReadCloser struct {
reader io.ReadCloser
}
func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) {
return s.reader, nil
}
func parseDataSource(source interface{}) (dataSource, error) {
switch s := source.(type) {
case string:
return sourceFile{s}, nil
case []byte:
return &sourceData{s}, nil
case io.ReadCloser:
return &sourceReadCloser{s}, nil
case io.Reader:
return &sourceReadCloser{ioutil.NopCloser(s)}, nil
default:
return nil, fmt.Errorf("error parsing data source: unknown type %q", s)
}
}
ini-1.67.0/deprecated.go 0000664 0000000 0000000 00000001427 14274172402 0015022 0 ustar 00root root 0000000 0000000 // Copyright 2019 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
var (
// Deprecated: Use "DefaultSection" instead.
DEFAULT_SECTION = DefaultSection
// Deprecated: AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE.
AllCapsUnderscore = SnackCase
)
ini-1.67.0/error.go 0000664 0000000 0000000 00000002731 14274172402 0014052 0 ustar 00root root 0000000 0000000 // Copyright 2016 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"fmt"
)
// ErrDelimiterNotFound indicates the error type of no delimiter is found which there should be one.
type ErrDelimiterNotFound struct {
Line string
}
// IsErrDelimiterNotFound returns true if the given error is an instance of ErrDelimiterNotFound.
func IsErrDelimiterNotFound(err error) bool {
_, ok := err.(ErrDelimiterNotFound)
return ok
}
func (err ErrDelimiterNotFound) Error() string {
return fmt.Sprintf("key-value delimiter not found: %s", err.Line)
}
// ErrEmptyKeyName indicates the error type of no key name is found which there should be one.
type ErrEmptyKeyName struct {
Line string
}
// IsErrEmptyKeyName returns true if the given error is an instance of ErrEmptyKeyName.
func IsErrEmptyKeyName(err error) bool {
_, ok := err.(ErrEmptyKeyName)
return ok
}
func (err ErrEmptyKeyName) Error() string {
return fmt.Sprintf("empty key name: %s", err.Line)
}
ini-1.67.0/file.go 0000664 0000000 0000000 00000033205 14274172402 0013640 0 ustar 00root root 0000000 0000000 // Copyright 2017 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"sync"
)
// File represents a combination of one or more INI files in memory.
type File struct {
options LoadOptions
dataSources []dataSource
// Should make things safe, but sometimes doesn't matter.
BlockMode bool
lock sync.RWMutex
// To keep data in order.
sectionList []string
// To keep track of the index of a section with same name.
// This meta list is only used with non-unique section names are allowed.
sectionIndexes []int
// Actual data is stored here.
sections map[string][]*Section
NameMapper
ValueMapper
}
// newFile initializes File object with given data sources.
func newFile(dataSources []dataSource, opts LoadOptions) *File {
if len(opts.KeyValueDelimiters) == 0 {
opts.KeyValueDelimiters = "=:"
}
if len(opts.KeyValueDelimiterOnWrite) == 0 {
opts.KeyValueDelimiterOnWrite = "="
}
if len(opts.ChildSectionDelimiter) == 0 {
opts.ChildSectionDelimiter = "."
}
return &File{
BlockMode: true,
dataSources: dataSources,
sections: make(map[string][]*Section),
options: opts,
}
}
// Empty returns an empty file object.
func Empty(opts ...LoadOptions) *File {
var opt LoadOptions
if len(opts) > 0 {
opt = opts[0]
}
// Ignore error here, we are sure our data is good.
f, _ := LoadSources(opt, []byte(""))
return f
}
// NewSection creates a new section.
func (f *File) NewSection(name string) (*Section, error) {
if len(name) == 0 {
return nil, errors.New("empty section name")
}
if (f.options.Insensitive || f.options.InsensitiveSections) && name != DefaultSection {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if !f.options.AllowNonUniqueSections && inSlice(name, f.sectionList) {
return f.sections[name][0], nil
}
f.sectionList = append(f.sectionList, name)
// NOTE: Append to indexes must happen before appending to sections,
// otherwise index will have off-by-one problem.
f.sectionIndexes = append(f.sectionIndexes, len(f.sections[name]))
sec := newSection(f, name)
f.sections[name] = append(f.sections[name], sec)
return sec, nil
}
// NewRawSection creates a new section with an unparseable body.
func (f *File) NewRawSection(name, body string) (*Section, error) {
section, err := f.NewSection(name)
if err != nil {
return nil, err
}
section.isRawSection = true
section.rawBody = body
return section, nil
}
// NewSections creates a list of sections.
func (f *File) NewSections(names ...string) (err error) {
for _, name := range names {
if _, err = f.NewSection(name); err != nil {
return err
}
}
return nil
}
// GetSection returns section by given name.
func (f *File) GetSection(name string) (*Section, error) {
secs, err := f.SectionsByName(name)
if err != nil {
return nil, err
}
return secs[0], err
}
// HasSection returns true if the file contains a section with given name.
func (f *File) HasSection(name string) bool {
section, _ := f.GetSection(name)
return section != nil
}
// SectionsByName returns all sections with given name.
func (f *File) SectionsByName(name string) ([]*Section, error) {
if len(name) == 0 {
name = DefaultSection
}
if f.options.Insensitive || f.options.InsensitiveSections {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
secs := f.sections[name]
if len(secs) == 0 {
return nil, fmt.Errorf("section %q does not exist", name)
}
return secs, nil
}
// Section assumes named section exists and returns a zero-value when not.
func (f *File) Section(name string) *Section {
sec, err := f.GetSection(name)
if err != nil {
if name == "" {
name = DefaultSection
}
sec, _ = f.NewSection(name)
return sec
}
return sec
}
// SectionWithIndex assumes named section exists and returns a new section when not.
func (f *File) SectionWithIndex(name string, index int) *Section {
secs, err := f.SectionsByName(name)
if err != nil || len(secs) <= index {
// NOTE: It's OK here because the only possible error is empty section name,
// but if it's empty, this piece of code won't be executed.
newSec, _ := f.NewSection(name)
return newSec
}
return secs[index]
}
// Sections returns a list of Section stored in the current instance.
func (f *File) Sections() []*Section {
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
sections := make([]*Section, len(f.sectionList))
for i, name := range f.sectionList {
sections[i] = f.sections[name][f.sectionIndexes[i]]
}
return sections
}
// ChildSections returns a list of child sections of given section name.
func (f *File) ChildSections(name string) []*Section {
return f.Section(name).ChildSections()
}
// SectionStrings returns list of section names.
func (f *File) SectionStrings() []string {
list := make([]string, len(f.sectionList))
copy(list, f.sectionList)
return list
}
// DeleteSection deletes a section or all sections with given name.
func (f *File) DeleteSection(name string) {
secs, err := f.SectionsByName(name)
if err != nil {
return
}
for i := 0; i < len(secs); i++ {
// For non-unique sections, it is always needed to remove the first one so
// in the next iteration, the subsequent section continue having index 0.
// Ignoring the error as index 0 never returns an error.
_ = f.DeleteSectionWithIndex(name, 0)
}
}
// DeleteSectionWithIndex deletes a section with given name and index.
func (f *File) DeleteSectionWithIndex(name string, index int) error {
if !f.options.AllowNonUniqueSections && index != 0 {
return fmt.Errorf("delete section with non-zero index is only allowed when non-unique sections is enabled")
}
if len(name) == 0 {
name = DefaultSection
}
if f.options.Insensitive || f.options.InsensitiveSections {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
// Count occurrences of the sections
occurrences := 0
sectionListCopy := make([]string, len(f.sectionList))
copy(sectionListCopy, f.sectionList)
for i, s := range sectionListCopy {
if s != name {
continue
}
if occurrences == index {
if len(f.sections[name]) <= 1 {
delete(f.sections, name) // The last one in the map
} else {
f.sections[name] = append(f.sections[name][:index], f.sections[name][index+1:]...)
}
// Fix section lists
f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...)
f.sectionIndexes = append(f.sectionIndexes[:i], f.sectionIndexes[i+1:]...)
} else if occurrences > index {
// Fix the indices of all following sections with this name.
f.sectionIndexes[i-1]--
}
occurrences++
}
return nil
}
func (f *File) reload(s dataSource) error {
r, err := s.ReadCloser()
if err != nil {
return err
}
defer r.Close()
return f.parse(r)
}
// Reload reloads and parses all data sources.
func (f *File) Reload() (err error) {
for _, s := range f.dataSources {
if err = f.reload(s); err != nil {
// In loose mode, we create an empty default section for nonexistent files.
if os.IsNotExist(err) && f.options.Loose {
_ = f.parse(bytes.NewBuffer(nil))
continue
}
return err
}
if f.options.ShortCircuit {
return nil
}
}
return nil
}
// Append appends one or more data sources and reloads automatically.
func (f *File) Append(source interface{}, others ...interface{}) error {
ds, err := parseDataSource(source)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
for _, s := range others {
ds, err = parseDataSource(s)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
}
return f.Reload()
}
func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
equalSign := DefaultFormatLeft + f.options.KeyValueDelimiterOnWrite + DefaultFormatRight
if PrettyFormat || PrettyEqual {
equalSign = fmt.Sprintf(" %s ", f.options.KeyValueDelimiterOnWrite)
}
// Use buffer to make sure target is safe until finish encoding.
buf := bytes.NewBuffer(nil)
lastSectionIdx := len(f.sectionList) - 1
for i, sname := range f.sectionList {
sec := f.SectionWithIndex(sname, f.sectionIndexes[i])
if len(sec.Comment) > 0 {
// Support multiline comments
lines := strings.Split(sec.Comment, LineBreak)
for i := range lines {
if lines[i][0] != '#' && lines[i][0] != ';' {
lines[i] = "; " + lines[i]
} else {
lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:])
}
if _, err := buf.WriteString(lines[i] + LineBreak); err != nil {
return nil, err
}
}
}
if i > 0 || DefaultHeader || (i == 0 && strings.ToUpper(sec.name) != DefaultSection) {
if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
return nil, err
}
} else {
// Write nothing if default section is empty
if len(sec.keyList) == 0 {
continue
}
}
isLastSection := i == lastSectionIdx
if sec.isRawSection {
if _, err := buf.WriteString(sec.rawBody); err != nil {
return nil, err
}
if PrettySection && !isLastSection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
continue
}
// Count and generate alignment length and buffer spaces using the
// longest key. Keys may be modified if they contain certain characters so
// we need to take that into account in our calculation.
alignLength := 0
if PrettyFormat {
for _, kname := range sec.keyList {
keyLength := len(kname)
// First case will surround key by ` and second by """
if strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters) {
keyLength += 2
} else if strings.Contains(kname, "`") {
keyLength += 6
}
if keyLength > alignLength {
alignLength = keyLength
}
}
}
alignSpaces := bytes.Repeat([]byte(" "), alignLength)
KeyList:
for _, kname := range sec.keyList {
key := sec.Key(kname)
if len(key.Comment) > 0 {
if len(indent) > 0 && sname != DefaultSection {
buf.WriteString(indent)
}
// Support multiline comments
lines := strings.Split(key.Comment, LineBreak)
for i := range lines {
if lines[i][0] != '#' && lines[i][0] != ';' {
lines[i] = "; " + strings.TrimSpace(lines[i])
} else {
lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:])
}
if _, err := buf.WriteString(lines[i] + LineBreak); err != nil {
return nil, err
}
}
}
if len(indent) > 0 && sname != DefaultSection {
buf.WriteString(indent)
}
switch {
case key.isAutoIncrement:
kname = "-"
case strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters):
kname = "`" + kname + "`"
case strings.Contains(kname, "`"):
kname = `"""` + kname + `"""`
}
writeKeyValue := func(val string) (bool, error) {
if _, err := buf.WriteString(kname); err != nil {
return false, err
}
if key.isBooleanType {
buf.WriteString(LineBreak)
return true, nil
}
// Write out alignment spaces before "=" sign
if PrettyFormat {
buf.Write(alignSpaces[:alignLength-len(kname)])
}
// In case key value contains "\n", "`", "\"", "#" or ";"
if strings.ContainsAny(val, "\n`") {
val = `"""` + val + `"""`
} else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") {
val = "`" + val + "`"
} else if len(strings.TrimSpace(val)) != len(val) {
val = `"` + val + `"`
}
if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil {
return false, err
}
return false, nil
}
shadows := key.ValueWithShadows()
if len(shadows) == 0 {
if _, err := writeKeyValue(""); err != nil {
return nil, err
}
}
for _, val := range shadows {
exitLoop, err := writeKeyValue(val)
if err != nil {
return nil, err
} else if exitLoop {
continue KeyList
}
}
for _, val := range key.nestedValues {
if _, err := buf.WriteString(indent + " " + val + LineBreak); err != nil {
return nil, err
}
}
}
if PrettySection && !isLastSection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
}
return buf, nil
}
// WriteToIndent writes content into io.Writer with given indention.
// If PrettyFormat has been set to be true,
// it will align "=" sign with spaces under each section.
func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) {
buf, err := f.writeToBuffer(indent)
if err != nil {
return 0, err
}
return buf.WriteTo(w)
}
// WriteTo writes file content into io.Writer.
func (f *File) WriteTo(w io.Writer) (int64, error) {
return f.WriteToIndent(w, "")
}
// SaveToIndent writes content to file system with given value indention.
func (f *File) SaveToIndent(filename, indent string) error {
// Note: Because we are truncating with os.Create,
// so it's safer to save to a temporary file location and rename after done.
buf, err := f.writeToBuffer(indent)
if err != nil {
return err
}
return ioutil.WriteFile(filename, buf.Bytes(), 0666)
}
// SaveTo writes content to file system.
func (f *File) SaveTo(filename string) error {
return f.SaveToIndent(filename, "")
}
ini-1.67.0/file_test.go 0000664 0000000 0000000 00000032063 14274172402 0014700 0 ustar 00root root 0000000 0000000 // Copyright 2017 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"io/ioutil"
"runtime"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEmpty(t *testing.T) {
f := Empty()
require.NotNil(t, f)
// Should only have the default section
assert.Len(t, f.Sections(), 1)
// Default section should not contain any key
assert.Len(t, f.Section("").Keys(), 0)
}
func TestFile_NewSection(t *testing.T) {
f := Empty()
require.NotNil(t, f)
sec, err := f.NewSection("author")
require.NoError(t, err)
require.NotNil(t, sec)
assert.Equal(t, "author", sec.Name())
assert.Equal(t, []string{DefaultSection, "author"}, f.SectionStrings())
t.Run("with duplicated name", func(t *testing.T) {
sec, err := f.NewSection("author")
require.NoError(t, err)
require.NotNil(t, sec)
// Does nothing if section already exists
assert.Equal(t, []string{DefaultSection, "author"}, f.SectionStrings())
})
t.Run("with empty string", func(t *testing.T) {
_, err := f.NewSection("")
require.Error(t, err)
})
}
func TestFile_NonUniqueSection(t *testing.T) {
t.Run("read and write non-unique sections", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowNonUniqueSections: true,
}, []byte(`[Interface]
Address = 192.168.2.1
PrivateKey =
ListenPort = 51820
[Peer]
PublicKey =
AllowedIPs = 192.168.2.2/32
[Peer]
PublicKey =
AllowedIPs = 192.168.2.3/32`))
require.NoError(t, err)
require.NotNil(t, f)
sec, err := f.NewSection("Peer")
require.NoError(t, err)
require.NotNil(t, f)
_, _ = sec.NewKey("PublicKey", "")
_, _ = sec.NewKey("AllowedIPs", "192.168.2.4/32")
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
str := buf.String()
assert.Equal(t, `[Interface]
Address = 192.168.2.1
PrivateKey =
ListenPort = 51820
[Peer]
PublicKey =
AllowedIPs = 192.168.2.2/32
[Peer]
PublicKey =
AllowedIPs = 192.168.2.3/32
[Peer]
PublicKey =
AllowedIPs = 192.168.2.4/32
`, str)
})
t.Run("delete non-unique section", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowNonUniqueSections: true,
}, []byte(`[Interface]
Address = 192.168.2.1
PrivateKey =
ListenPort = 51820
[Peer]
PublicKey =
AllowedIPs = 192.168.2.2/32
[Peer]
PublicKey =
AllowedIPs = 192.168.2.3/32
[Peer]
PublicKey =
AllowedIPs = 192.168.2.4/32
`))
require.NoError(t, err)
require.NotNil(t, f)
err = f.DeleteSectionWithIndex("Peer", 1)
require.NoError(t, err)
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
str := buf.String()
assert.Equal(t, `[Interface]
Address = 192.168.2.1
PrivateKey =
ListenPort = 51820
[Peer]
PublicKey =
AllowedIPs = 192.168.2.2/32
[Peer]
PublicKey =
AllowedIPs = 192.168.2.4/32
`, str)
})
t.Run("delete all sections", func(t *testing.T) {
f := Empty(LoadOptions{
AllowNonUniqueSections: true,
})
require.NotNil(t, f)
_ = f.NewSections("Interface", "Peer", "Peer")
assert.Equal(t, []string{DefaultSection, "Interface", "Peer", "Peer"}, f.SectionStrings())
f.DeleteSection("Peer")
assert.Equal(t, []string{DefaultSection, "Interface"}, f.SectionStrings())
})
}
func TestFile_NewRawSection(t *testing.T) {
f := Empty()
require.NotNil(t, f)
sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000`)
require.NoError(t, err)
require.NotNil(t, sec)
assert.Equal(t, "comments", sec.Name())
assert.Equal(t, []string{DefaultSection, "comments"}, f.SectionStrings())
assert.Equal(t, `1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000`, f.Section("comments").Body())
t.Run("with duplicated name", func(t *testing.T) {
sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000`)
require.NoError(t, err)
require.NotNil(t, sec)
assert.Equal(t, []string{DefaultSection, "comments"}, f.SectionStrings())
// Overwrite previous existed section
assert.Equal(t, `1111111111111111111000000000000000001110000`, f.Section("comments").Body())
})
t.Run("with empty string", func(t *testing.T) {
_, err := f.NewRawSection("", "")
require.Error(t, err)
})
}
func TestFile_NewSections(t *testing.T) {
f := Empty()
require.NotNil(t, f)
assert.NoError(t, f.NewSections("package", "author"))
assert.Equal(t, []string{DefaultSection, "package", "author"}, f.SectionStrings())
t.Run("with duplicated name", func(t *testing.T) {
assert.NoError(t, f.NewSections("author", "features"))
// Ignore section already exists
assert.Equal(t, []string{DefaultSection, "package", "author", "features"}, f.SectionStrings())
})
t.Run("with empty string", func(t *testing.T) {
assert.Error(t, f.NewSections("", ""))
})
}
func TestFile_GetSection(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
sec, err := f.GetSection("author")
require.NoError(t, err)
require.NotNil(t, sec)
assert.Equal(t, "author", sec.Name())
t.Run("section not exists", func(t *testing.T) {
_, err := f.GetSection("404")
require.Error(t, err)
})
}
func TestFile_HasSection(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
sec := f.HasSection("author")
assert.True(t, sec)
t.Run("section not exists", func(t *testing.T) {
nonexistent := f.HasSection("404")
assert.False(t, nonexistent)
})
}
func TestFile_Section(t *testing.T) {
t.Run("get a section", func(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
sec := f.Section("author")
require.NotNil(t, sec)
assert.Equal(t, "author", sec.Name())
t.Run("section not exists", func(t *testing.T) {
sec := f.Section("404")
require.NotNil(t, sec)
assert.Equal(t, "404", sec.Name())
})
})
t.Run("get default section in lower case with insensitive load", func(t *testing.T) {
f, err := InsensitiveLoad([]byte(`
[default]
NAME = ini
VERSION = v1`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "ini", f.Section("").Key("name").String())
assert.Equal(t, "v1", f.Section("").Key("version").String())
})
t.Run("get sections after deletion", func(t *testing.T) {
f, err := Load([]byte(`
[RANDOM]
`))
require.NoError(t, err)
require.NotNil(t, f)
sectionNames := f.SectionStrings()
sort.Strings(sectionNames)
assert.Equal(t, []string{DefaultSection, "RANDOM"}, sectionNames)
for _, currentSection := range sectionNames {
f.DeleteSection(currentSection)
}
for sectionParam, expectedSectionName := range map[string]string{
"": DefaultSection,
"RANDOM": "RANDOM",
} {
sec := f.Section(sectionParam)
require.NotNil(t, sec)
assert.Equal(t, expectedSectionName, sec.Name())
}
})
}
func TestFile_Sections(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
secs := f.Sections()
names := []string{DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}
assert.Len(t, secs, len(names))
for i, name := range names {
assert.Equal(t, name, secs[i].Name())
}
}
func TestFile_ChildSections(t *testing.T) {
f, err := Load([]byte(`
[node]
[node.biz1]
[node.biz2]
[node.biz3]
[node.bizN]
`))
require.NoError(t, err)
require.NotNil(t, f)
children := f.ChildSections("node")
names := []string{"node.biz1", "node.biz2", "node.biz3", "node.bizN"}
assert.Len(t, children, len(names))
for i, name := range names {
assert.Equal(t, name, children[i].Name())
}
}
func TestFile_SectionStrings(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, []string{DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"}, f.SectionStrings())
}
func TestFile_DeleteSection(t *testing.T) {
t.Run("delete a section", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
_ = f.NewSections("author", "package", "features")
f.DeleteSection("features")
f.DeleteSection("")
assert.Equal(t, []string{"author", "package"}, f.SectionStrings())
})
t.Run("delete default section", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
f.Section("").Key("foo").SetValue("bar")
f.Section("section1").Key("key1").SetValue("value1")
f.DeleteSection("")
assert.Equal(t, []string{"section1"}, f.SectionStrings())
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[section1]
key1 = value1
`, buf.String())
})
t.Run("delete a section with InsensitiveSections", func(t *testing.T) {
f := Empty(LoadOptions{InsensitiveSections: true})
require.NotNil(t, f)
_ = f.NewSections("author", "package", "features")
f.DeleteSection("FEATURES")
f.DeleteSection("")
assert.Equal(t, []string{"author", "package"}, f.SectionStrings())
})
}
func TestFile_Append(t *testing.T) {
f := Empty()
require.NotNil(t, f)
assert.NoError(t, f.Append(minimalConf, []byte(`
[author]
NAME = Unknwon`)))
t.Run("with bad input", func(t *testing.T) {
assert.Error(t, f.Append(123))
assert.Error(t, f.Append(minimalConf, 123))
})
}
func TestFile_WriteTo(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")
}
t.Run("write content to somewhere", func(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
f.Section("author").Comment = `Information about package author
# Bio can be written in multiple lines.`
f.Section("author").Key("NAME").Comment = "This is author name"
_, _ = f.Section("note").NewBooleanKey("boolean_key")
_, _ = f.Section("note").NewKey("more", "notes")
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
golden := "testdata/TestFile_WriteTo.golden"
if *update {
require.NoError(t, ioutil.WriteFile(golden, buf.Bytes(), 0644))
}
expected, err := ioutil.ReadFile(golden)
require.NoError(t, err)
assert.Equal(t, string(expected), buf.String())
})
t.Run("support multiline comments", func(t *testing.T) {
f, err := Load([]byte(`
#
# general.domain
#
# Domain name of XX system.
domain = mydomain.com
`))
require.NoError(t, err)
f.Section("").Key("test").Comment = "Multiline\nComment"
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `#
# general.domain
#
# Domain name of XX system.
domain = mydomain.com
; Multiline
; Comment
test =
`, buf.String())
})
t.Run("keep leading and trailing spaces in value", func(t *testing.T) {
f, _ := Load([]byte(`[foo]
bar1 = ' val ue1 '
bar2 = """ val ue2 """
bar3 = " val ue3 "
`))
require.NotNil(t, f)
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[foo]
bar1 = " val ue1 "
bar2 = " val ue2 "
bar3 = " val ue3 "
`, buf.String())
})
}
func TestFile_SaveTo(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.NoError(t, f.SaveTo("testdata/conf_out.ini"))
assert.NoError(t, f.SaveToIndent("testdata/conf_out.ini", "\t"))
}
func TestFile_WriteToWithOutputDelimiter(t *testing.T) {
f, err := LoadSources(LoadOptions{
KeyValueDelimiterOnWrite: "->",
}, []byte(`[Others]
Cities = HangZhou|Boston
Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
Years = 1993,1994
Numbers = 10010,10086
Ages = 18,19
Populations = 12345678,98765432
Coordinates = 192.168,10.11
Flags = true,false
Note = Hello world!`))
require.NoError(t, err)
require.NotNil(t, f)
var actual bytes.Buffer
var expected = []byte(`[Others]
Cities -> HangZhou|Boston
Visits -> 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
Years -> 1993,1994
Numbers -> 10010,10086
Ages -> 18,19
Populations -> 12345678,98765432
Coordinates -> 192.168,10.11
Flags -> true,false
Note -> Hello world!
`)
_, err = f.WriteTo(&actual)
require.NoError(t, err)
assert.Equal(t, expected, actual.Bytes())
}
// Inspired by https://github.com/go-ini/ini/issues/207
func TestReloadAfterShadowLoad(t *testing.T) {
f, err := ShadowLoad([]byte(`
[slice]
v = 1
v = 2
v = 3
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, []string{"1", "2", "3"}, f.Section("slice").Key("v").ValueWithShadows())
require.NoError(t, f.Reload())
assert.Equal(t, []string{"1", "2", "3"}, f.Section("slice").Key("v").ValueWithShadows())
}
ini-1.67.0/helper.go 0000664 0000000 0000000 00000001317 14274172402 0014177 0 ustar 00root root 0000000 0000000 // Copyright 2019 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
func inSlice(str string, s []string) bool {
for _, v := range s {
if str == v {
return true
}
}
return false
}
ini-1.67.0/helper_test.go 0000664 0000000 0000000 00000001435 14274172402 0015237 0 ustar 00root root 0000000 0000000 // Copyright 2019 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsInSlice(t *testing.T) {
ss := []string{"a", "b", "c"}
assert.True(t, inSlice("a", ss))
assert.False(t, inSlice("d", ss))
}
ini-1.67.0/ini.go 0000664 0000000 0000000 00000017563 14274172402 0013511 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package ini provides INI file read and write functionality in Go.
package ini
import (
"os"
"regexp"
"runtime"
"strings"
)
const (
// Maximum allowed depth when recursively substituing variable names.
depthValues = 99
)
var (
// DefaultSection is the name of default section. You can use this var or the string literal.
// In most of cases, an empty string is all you need to access the section.
DefaultSection = "DEFAULT"
// LineBreak is the delimiter to determine or compose a new line.
// This variable will be changed to "\r\n" automatically on Windows at package init time.
LineBreak = "\n"
// Variable regexp pattern: %(variable)s
varPattern = regexp.MustCompile(`%\(([^)]+)\)s`)
// DefaultHeader explicitly writes default section header.
DefaultHeader = false
// PrettySection indicates whether to put a line between sections.
PrettySection = true
// PrettyFormat indicates whether to align "=" sign with spaces to produce pretty output
// or reduce all possible spaces for compact format.
PrettyFormat = true
// PrettyEqual places spaces around "=" sign even when PrettyFormat is false.
PrettyEqual = false
// DefaultFormatLeft places custom spaces on the left when PrettyFormat and PrettyEqual are both disabled.
DefaultFormatLeft = ""
// DefaultFormatRight places custom spaces on the right when PrettyFormat and PrettyEqual are both disabled.
DefaultFormatRight = ""
)
var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
func init() {
if runtime.GOOS == "windows" && !inTest {
LineBreak = "\r\n"
}
}
// LoadOptions contains all customized options used for load data source(s).
type LoadOptions struct {
// Loose indicates whether the parser should ignore nonexistent files or return error.
Loose bool
// Insensitive indicates whether the parser forces all section and key names to lowercase.
Insensitive bool
// InsensitiveSections indicates whether the parser forces all section to lowercase.
InsensitiveSections bool
// InsensitiveKeys indicates whether the parser forces all key names to lowercase.
InsensitiveKeys bool
// IgnoreContinuation indicates whether to ignore continuation lines while parsing.
IgnoreContinuation bool
// IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value.
IgnoreInlineComment bool
// SkipUnrecognizableLines indicates whether to skip unrecognizable lines that do not conform to key/value pairs.
SkipUnrecognizableLines bool
// ShortCircuit indicates whether to ignore other configuration sources after loaded the first available configuration source.
ShortCircuit bool
// AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
// This type of keys are mostly used in my.cnf.
AllowBooleanKeys bool
// AllowShadows indicates whether to keep track of keys with same name under same section.
AllowShadows bool
// 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
// SpaceBeforeInlineComment indicates whether to allow comment symbols (\# and \;) inside value.
// Docs: https://docs.python.org/2/library/configparser.html
// Quote: Comments may appear on their own in an otherwise empty line, or may be entered in lines holding values or section names.
// In the latter case, they need to be preceded by a whitespace character to be recognized as a comment.
SpaceBeforeInlineComment 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
// UnescapeValueCommentSymbols indicates to unescape comment symbols (\# and \;) inside value to regular format
// when value is NOT surrounded by any quotes.
// Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all.
UnescapeValueCommentSymbols bool
// UnparseableSections stores a list of blocks that are allowed with raw content which do not otherwise
// conform to key/value pairs. Specify the names of those blocks here.
UnparseableSections []string
// KeyValueDelimiters is the sequence of delimiters that are used to separate key and value. By default, it is "=:".
KeyValueDelimiters string
// KeyValueDelimiterOnWrite is the delimiter that are used to separate key and value output. By default, it is "=".
KeyValueDelimiterOnWrite string
// ChildSectionDelimiter is the delimiter that is used to separate child sections. By default, it is ".".
ChildSectionDelimiter string
// PreserveSurroundedQuote indicates whether to preserve surrounded quote (single and double quotes).
PreserveSurroundedQuote bool
// DebugFunc is called to collect debug information (currently only useful to debug parsing Python-style multiline values).
DebugFunc DebugFunc
// ReaderBufferSize is the buffer size of the reader in bytes.
ReaderBufferSize int
// AllowNonUniqueSections indicates whether to allow sections with the same name multiple times.
AllowNonUniqueSections bool
// AllowDuplicateShadowValues indicates whether values for shadowed keys should be deduplicated.
AllowDuplicateShadowValues bool
}
// DebugFunc is the type of function called to log parse events.
type DebugFunc func(message string)
// LoadSources allows caller to apply customized options for loading from data source(s).
func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) {
sources := make([]dataSource, len(others)+1)
sources[0], err = parseDataSource(source)
if err != nil {
return nil, err
}
for i := range others {
sources[i+1], err = parseDataSource(others[i])
if err != nil {
return nil, err
}
}
f := newFile(sources, opts)
if err = f.Reload(); err != nil {
return nil, err
}
return f, nil
}
// Load loads and parses from INI data sources.
// Arguments can be mixed of file name with string type, or raw data in []byte.
// It will return error if list contains nonexistent files.
func Load(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{}, source, others...)
}
// LooseLoad has exactly same functionality as Load function
// except it ignores nonexistent files instead of returning error.
func LooseLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{Loose: true}, source, others...)
}
// InsensitiveLoad has exactly same functionality as Load function
// except it forces all section and key names to be lowercased.
func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{Insensitive: true}, source, others...)
}
// ShadowLoad has exactly same functionality as Load function
// except it allows have shadow keys.
func ShadowLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{AllowShadows: true}, source, others...)
}
ini-1.67.0/ini_test.go 0000664 0000000 0000000 00000140417 14274172402 0014543 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"flag"
"io/ioutil"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
confData = `
; Package name
NAME = ini
; Package version
VERSION = v1
; Package import path
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
# Information about package author
# Bio can be written in multiple lines.
[author]
NAME = Unknwon ; Succeeding comment
E-MAIL = fake@localhost
GITHUB = https://github.com/%(NAME)s
BIO = """Gopher.
Coding addict.
Good man.
""" # Succeeding comment`
minimalConf = "testdata/minimal.ini"
fullConf = "testdata/full.ini"
notFoundConf = "testdata/404.ini"
)
var update = flag.Bool("update", false, "Update .golden files")
func TestLoad(t *testing.T) {
t.Run("load from good data sources", func(t *testing.T) {
f, err := Load(
"testdata/minimal.ini",
[]byte("NAME = ini\nIMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s"),
bytes.NewReader([]byte(`VERSION = v1`)),
ioutil.NopCloser(bytes.NewReader([]byte("[author]\nNAME = Unknwon"))),
)
require.NoError(t, err)
require.NotNil(t, f)
// Validate values make sure all sources are loaded correctly
sec := f.Section("")
assert.Equal(t, "ini", sec.Key("NAME").String())
assert.Equal(t, "v1", sec.Key("VERSION").String())
assert.Equal(t, "gopkg.in/ini.v1", sec.Key("IMPORT_PATH").String())
sec = f.Section("author")
assert.Equal(t, "Unknwon", sec.Key("NAME").String())
assert.Equal(t, "u@gogs.io", sec.Key("E-MAIL").String())
})
t.Run("load from bad data sources", func(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
_, err := Load(notFoundConf)
require.Error(t, err)
})
t.Run("unsupported type", func(t *testing.T) {
_, err := Load(123)
require.Error(t, err)
})
})
t.Run("cannot properly parse INI files containing `#` or `;` in value", func(t *testing.T) {
f, err := Load([]byte(`
[author]
NAME = U#n#k#n#w#o#n
GITHUB = U;n;k;n;w;o;n
`))
require.NoError(t, err)
require.NotNil(t, f)
sec := f.Section("author")
nameValue := sec.Key("NAME").String()
githubValue := sec.Key("GITHUB").String()
assert.Equal(t, "U", nameValue)
assert.Equal(t, "U", githubValue)
})
t.Run("cannot parse small python-compatible INI files", func(t *testing.T) {
f, err := Load([]byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
foo
bar
foobar
barfoo
-----END RSA PRIVATE KEY-----
`))
require.Error(t, err)
assert.Nil(t, f)
assert.Equal(t, "key-value delimiter not found: foo\n", err.Error())
})
t.Run("cannot parse big python-compatible INI files", func(t *testing.T) {
f, err := 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-----
`))
require.Error(t, err)
assert.Nil(t, f)
assert.Equal(t, "key-value delimiter not found: 1foo\n", err.Error())
})
}
func TestLooseLoad(t *testing.T) {
f, err := LoadSources(LoadOptions{Loose: true}, notFoundConf, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
t.Run("inverse case", func(t *testing.T) {
_, err = Load(notFoundConf)
require.Error(t, err)
})
}
func TestInsensitiveLoad(t *testing.T) {
t.Run("insensitive to section and key names", func(t *testing.T) {
f, err := InsensitiveLoad(minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "u@gogs.io", f.Section("Author").Key("e-mail").String())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[author]
e-mail = u@gogs.io
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
f, err := Load(minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Empty(t, f.Section("Author").Key("e-mail").String())
})
})
// Ref: https://github.com/go-ini/ini/issues/198
t.Run("insensitive load with default section", func(t *testing.T) {
f, err := InsensitiveLoad([]byte(`
user = unknwon
[profile]
email = unknwon@local
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "unknwon", f.Section(DefaultSection).Key("user").String())
})
}
func TestLoadSources(t *testing.T) {
t.Run("with true `AllowPythonMultilineValues`", func(t *testing.T) {
t.Run("ignore nonexistent files", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true, Loose: true}, notFoundConf, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
t.Run("inverse case", func(t *testing.T) {
_, err = LoadSources(LoadOptions{AllowPythonMultilineValues: true}, notFoundConf)
require.Error(t, err)
})
})
t.Run("insensitive to section and key names", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true, Insensitive: true}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "u@gogs.io", f.Section("Author").Key("e-mail").String())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[author]
e-mail = u@gogs.io
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Empty(t, f.Section("Author").Key("e-mail").String())
})
})
t.Run("insensitive to sections and sensitive to key names", func(t *testing.T) {
f, err := LoadSources(LoadOptions{InsensitiveSections: true}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "u@gogs.io", f.Section("Author").Key("E-MAIL").String())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[author]
E-MAIL = u@gogs.io
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Empty(t, f.Section("Author").Key("e-mail").String())
})
})
t.Run("sensitive to sections and insensitive to key names", func(t *testing.T) {
f, err := LoadSources(LoadOptions{InsensitiveKeys: true}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "u@gogs.io", f.Section("author").Key("e-mail").String())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[author]
e-mail = u@gogs.io
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Empty(t, f.Section("Author").Key("e-mail").String())
})
})
t.Run("ignore continuation lines", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
IgnoreContinuation: true,
}, []byte(`
key1=a\b\
key2=c\d\
key3=value`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `a\b\`, f.Section("").Key("key1").String())
assert.Equal(t, `c\d\`, f.Section("").Key("key2").String())
assert.Equal(t, "value", f.Section("").Key("key3").String())
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
key1=a\b\
key2=c\d\`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `a\bkey2=c\d`, f.Section("").Key("key1").String())
})
})
t.Run("ignore inline comments", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
IgnoreInlineComment: true,
}, []byte(`
key1=value ;comment
key2=value2 #comment2
key3=val#ue #comment3`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `value ;comment`, f.Section("").Key("key1").String())
assert.Equal(t, `value2 #comment2`, f.Section("").Key("key2").String())
assert.Equal(t, `val#ue #comment3`, f.Section("").Key("key3").String())
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
key1=value ;comment
key2=value2 #comment2`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `value`, f.Section("").Key("key1").String())
assert.Equal(t, `;comment`, f.Section("").Key("key1").Comment)
assert.Equal(t, `value2`, f.Section("").Key("key2").String())
assert.Equal(t, `#comment2`, f.Section("").Key("key2").Comment)
})
})
t.Run("skip unrecognizable lines", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
SkipUnrecognizableLines: true,
}, []byte(`
GenerationDepth: 13
BiomeRarityScale: 100
################
# Biome Groups #
################
BiomeGroup(NormalBiomes, 3, 99, RoofedForestEnchanted, ForestSakura, FloatingJungle
BiomeGroup(IceBiomes, 4, 85, Ice Plains)
= RainForest
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "13", f.Section("").Key("GenerationDepth").String())
assert.Equal(t, "100", f.Section("").Key("BiomeRarityScale").String())
assert.False(t, f.Section("").HasKey("BiomeGroup"))
})
t.Run("allow boolean type keys", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
AllowBooleanKeys: true,
}, []byte(`
key1=hello
#key2
key3`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, []string{"key1", "key3"}, f.Section("").KeyStrings())
assert.True(t, f.Section("").Key("key3").MustBool(false))
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `key1 = hello
# key2
key3
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
key1=hello
#key2
key3`))
require.Error(t, err)
})
})
t.Run("allow shadow keys", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowShadows: true, AllowPythonMultilineValues: true}, []byte(`
[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git
fetch = +refs/heads/*:refs/remotes/origin/*`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "https://github.com/Antergone/test1.git", f.Section(`remote "origin"`).Key("url").String())
assert.Equal(
t,
[]string{
"https://github.com/Antergone/test1.git",
"https://github.com/Antergone/test2.git",
},
f.Section(`remote "origin"`).Key("url").ValueWithShadows(),
)
assert.Equal(t, "+refs/heads/*:refs/remotes/origin/*", f.Section(`remote "origin"`).Key("fetch").String())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "https://github.com/Antergone/test2.git", f.Section(`remote "origin"`).Key("url").String())
})
})
t.Run("unescape double quotes inside value", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
UnescapeValueDoubleQuotes: true,
}, []byte(`
create_repo="创建了仓库 %s"`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `创建了仓库 %s`, f.Section("").Key("create_repo").String())
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
create_repo="创建了仓库 %s"`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `"创建了仓库 %s"`, f.Section("").Key("create_repo").String())
})
})
t.Run("unescape comment symbols inside value", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, []byte(`
key = test value more text
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `test value more text`, f.Section("").Key("key").String())
})
t.Run("can parse small python-compatible INI files", func(t *testing.T) {
f, err := LoadSources(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-----
multiline_list =
first
second
third
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "-----BEGIN RSA PRIVATE KEY-----\n foo\n bar\n foobar\n barfoo\n -----END RSA PRIVATE KEY-----", f.Section("long").Key("long_rsa_private_key").String())
assert.Equal(t, "\n first\n second\n third", f.Section("long").Key("multiline_list").String())
})
t.Run("can parse big python-compatible INI files", func(t *testing.T) {
f, err := LoadSources(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-----
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `-----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-----`,
f.Section("long").Key("long_rsa_private_key").String(),
)
})
t.Run("allow unparsable sections", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
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> This slide has the fuel listed in the wrong units
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "3", f.Section("").Key("score").String())
assert.Empty(t, f.Section("").Body())
assert.Equal(t, `my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data`,
f.Section("core_lesson").Body(),
)
assert.Equal(t, `<1> This slide has the fuel listed in the wrong units `, f.Section("comments").Body())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `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> This slide has the fuel listed in the wrong units
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: true}, []byte(`
[CORE_LESSON]
my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data`))
require.Error(t, err)
})
})
t.Run("and false `SpaceBeforeInlineComment`", func(t *testing.T) {
t.Run("cannot parse INI files containing `#` or `;` in value", func(t *testing.T) {
f, err := LoadSources(
LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: false},
[]byte(`
[author]
NAME = U#n#k#n#w#o#n
GITHUB = U;n;k;n;w;o;n
`))
require.NoError(t, err)
require.NotNil(t, f)
sec := f.Section("author")
nameValue := sec.Key("NAME").String()
githubValue := sec.Key("GITHUB").String()
assert.Equal(t, "U", nameValue)
assert.Equal(t, "U", githubValue)
})
})
t.Run("and true `SpaceBeforeInlineComment`", func(t *testing.T) {
t.Run("can parse INI files containing `#` or `;` in value", func(t *testing.T) {
f, err := LoadSources(
LoadOptions{AllowPythonMultilineValues: false, SpaceBeforeInlineComment: true},
[]byte(`
[author]
NAME = U#n#k#n#w#o#n
GITHUB = U;n;k;n;w;o;n
`))
require.NoError(t, err)
require.NotNil(t, f)
sec := f.Section("author")
nameValue := sec.Key("NAME").String()
githubValue := sec.Key("GITHUB").String()
assert.Equal(t, "U#n#k#n#w#o#n", nameValue)
assert.Equal(t, "U;n;k;n;w;o;n", githubValue)
})
})
})
t.Run("with false `AllowPythonMultilineValues`", func(t *testing.T) {
t.Run("ignore nonexistent files", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
Loose: true,
}, notFoundConf, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
t.Run("inverse case", func(t *testing.T) {
_, err = LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
}, notFoundConf)
require.Error(t, err)
})
})
t.Run("insensitive to section and key names", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
Insensitive: true,
}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "u@gogs.io", f.Section("Author").Key("e-mail").String())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[author]
e-mail = u@gogs.io
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
}, minimalConf)
require.NoError(t, err)
require.NotNil(t, f)
assert.Empty(t, f.Section("Author").Key("e-mail").String())
})
})
t.Run("ignore continuation lines", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
IgnoreContinuation: true,
}, []byte(`
key1=a\b\
key2=c\d\
key3=value`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `a\b\`, f.Section("").Key("key1").String())
assert.Equal(t, `c\d\`, f.Section("").Key("key2").String())
assert.Equal(t, "value", f.Section("").Key("key3").String())
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
key1=a\b\
key2=c\d\`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `a\bkey2=c\d`, f.Section("").Key("key1").String())
})
})
t.Run("ignore inline comments", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
IgnoreInlineComment: true,
}, []byte(`
key1=value ;comment
key2=value2 #comment2
key3=val#ue #comment3`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `value ;comment`, f.Section("").Key("key1").String())
assert.Equal(t, `value2 #comment2`, f.Section("").Key("key2").String())
assert.Equal(t, `val#ue #comment3`, f.Section("").Key("key3").String())
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
key1=value ;comment
key2=value2 #comment2`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `value`, f.Section("").Key("key1").String())
assert.Equal(t, `;comment`, f.Section("").Key("key1").Comment)
assert.Equal(t, `value2`, f.Section("").Key("key2").String())
assert.Equal(t, `#comment2`, f.Section("").Key("key2").Comment)
})
})
t.Run("allow boolean type keys", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
AllowBooleanKeys: true,
}, []byte(`
key1=hello
#key2
key3`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, []string{"key1", "key3"}, f.Section("").KeyStrings())
assert.True(t, f.Section("").Key("key3").MustBool(false))
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `key1 = hello
# key2
key3
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
key1=hello
#key2
key3`))
require.Error(t, err)
})
})
t.Run("allow shadow keys", func(t *testing.T) {
f, err := LoadSources(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/*`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "https://github.com/Antergone/test1.git", f.Section(`remote "origin"`).Key("url").String())
assert.Equal(
t,
[]string{
"https://github.com/Antergone/test1.git",
"https://github.com/Antergone/test2.git",
},
f.Section(`remote "origin"`).Key("url").ValueWithShadows(),
)
assert.Equal(t, "+refs/heads/*:refs/remotes/origin/*", f.Section(`remote "origin"`).Key("fetch").String())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
[remote "origin"]
url = https://github.com/Antergone/test1.git
url = https://github.com/Antergone/test2.git`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "https://github.com/Antergone/test2.git", f.Section(`remote "origin"`).Key("url").String())
})
})
t.Run("unescape double quotes inside value", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
UnescapeValueDoubleQuotes: true,
}, []byte(`
create_repo="创建了仓库 %s"`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `创建了仓库 %s`, f.Section("").Key("create_repo").String())
t.Run("inverse case", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
create_repo="创建了仓库 %s"`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `"创建了仓库 %s"`, f.Section("").Key("create_repo").String())
})
})
t.Run("unescape comment symbols inside value", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: false,
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
}, []byte(`
key = test value more text
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, `test value more text`, f.Section("").Key("key").String())
})
t.Run("cannot parse small python-compatible INI files", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
[long]
long_rsa_private_key = -----BEGIN RSA PRIVATE KEY-----
foo
bar
foobar
barfoo
-----END RSA PRIVATE KEY-----
`))
require.Error(t, err)
assert.Nil(t, f)
assert.Equal(t, "key-value delimiter not found: foo\n", err.Error())
})
t.Run("cannot parse big python-compatible INI files", func(t *testing.T) {
f, err := LoadSources(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-----
`))
require.Error(t, err)
assert.Nil(t, f)
assert.Equal(t, "key-value delimiter not found: 1foo\n", err.Error())
})
t.Run("allow unparsable sections", func(t *testing.T) {
f, err := LoadSources(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> This slide has the fuel listed in the wrong units
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "3", f.Section("").Key("score").String())
assert.Empty(t, f.Section("").Body())
assert.Equal(t, `my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data`,
f.Section("core_lesson").Body(),
)
assert.Equal(t, `<1> This slide has the fuel listed in the wrong units `, f.Section("comments").Body())
t.Run("write out", func(t *testing.T) {
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `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> This slide has the fuel listed in the wrong units
`,
buf.String(),
)
})
t.Run("inverse case", func(t *testing.T) {
_, err := LoadSources(LoadOptions{AllowPythonMultilineValues: false}, []byte(`
[CORE_LESSON]
my lesson state data – 1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000 – end my lesson state data`))
require.Error(t, err)
})
})
t.Run("and false `SpaceBeforeInlineComment`", func(t *testing.T) {
t.Run("cannot parse INI files containing `#` or `;` in value", func(t *testing.T) {
f, err := LoadSources(
LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: false},
[]byte(`
[author]
NAME = U#n#k#n#w#o#n
GITHUB = U;n;k;n;w;o;n
`))
require.NoError(t, err)
require.NotNil(t, f)
sec := f.Section("author")
nameValue := sec.Key("NAME").String()
githubValue := sec.Key("GITHUB").String()
assert.Equal(t, "U", nameValue)
assert.Equal(t, "U", githubValue)
})
})
t.Run("and true `SpaceBeforeInlineComment`", func(t *testing.T) {
t.Run("can parse INI files containing `#` or `;` in value", func(t *testing.T) {
f, err := LoadSources(
LoadOptions{AllowPythonMultilineValues: true, SpaceBeforeInlineComment: true},
[]byte(`
[author]
NAME = U#n#k#n#w#o#n
GITHUB = U;n;k;n;w;o;n
`))
require.NoError(t, err)
require.NotNil(t, f)
sec := f.Section("author")
nameValue := sec.Key("NAME").String()
githubValue := sec.Key("GITHUB").String()
assert.Equal(t, "U#n#k#n#w#o#n", nameValue)
assert.Equal(t, "U;n;k;n;w;o;n", githubValue)
})
})
})
t.Run("with `ChildSectionDelimiter` ':'", func(t *testing.T) {
t.Run("get all keys of parent sections", func(t *testing.T) {
f := Empty(LoadOptions{ChildSectionDelimiter: ":"})
require.NotNil(t, f)
k, err := f.Section("package").NewKey("NAME", "ini")
require.NoError(t, err)
assert.NotNil(t, k)
k, err = f.Section("package").NewKey("VERSION", "v1")
require.NoError(t, err)
assert.NotNil(t, k)
k, err = f.Section("package").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
require.NoError(t, err)
assert.NotNil(t, k)
keys := f.Section("package:sub:sub2").ParentKeys()
names := []string{"NAME", "VERSION", "IMPORT_PATH"}
assert.Equal(t, len(names), len(keys))
for i, name := range names {
assert.Equal(t, name, keys[i].Name())
}
})
t.Run("getting and setting values", func(t *testing.T) {
f, err := LoadSources(LoadOptions{ChildSectionDelimiter: ":"}, fullConf)
require.NoError(t, err)
require.NotNil(t, f)
t.Run("get parent-keys that are available to the child section", func(t *testing.T) {
parentKeys := f.Section("package:sub").ParentKeys()
assert.NotNil(t, parentKeys)
for _, k := range parentKeys {
assert.Equal(t, "CLONE_URL", k.Name())
}
})
t.Run("get parent section value", func(t *testing.T) {
assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package:sub").Key("CLONE_URL").String())
assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package:fake:sub").Key("CLONE_URL").String())
})
})
t.Run("get child sections by parent name", func(t *testing.T) {
f, err := LoadSources(LoadOptions{ChildSectionDelimiter: ":"}, []byte(`
[node]
[node:biz1]
[node:biz2]
[node.biz3]
[node.bizN]
`))
require.NoError(t, err)
require.NotNil(t, f)
children := f.ChildSections("node")
names := []string{"node:biz1", "node:biz2"}
assert.Equal(t, len(names), len(children))
for i, name := range names {
assert.Equal(t, name, children[i].Name())
}
})
})
t.Run("ShortCircuit", func(t *testing.T) {
t.Run("load the first available configuration, ignore other configuration", func(t *testing.T) {
f, err := LoadSources(LoadOptions{ShortCircuit: true}, minimalConf, []byte(`key1 = value1`))
require.NotNil(t, f)
require.NoError(t, err)
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[author]
E-MAIL = u@gogs.io
`,
buf.String(),
)
})
t.Run("return an error when fail to load", func(t *testing.T) {
f, err := LoadSources(LoadOptions{ShortCircuit: true}, notFoundConf, minimalConf)
assert.Nil(t, f)
require.Error(t, err)
})
t.Run("used with Loose to ignore errors that the file does not exist", func(t *testing.T) {
f, err := LoadSources(LoadOptions{ShortCircuit: true, Loose: true}, notFoundConf, minimalConf)
require.NotNil(t, f)
require.NoError(t, err)
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[author]
E-MAIL = u@gogs.io
`,
buf.String(),
)
})
t.Run("ensure all sources are loaded without ShortCircuit", func(t *testing.T) {
f, err := LoadSources(LoadOptions{ShortCircuit: false}, minimalConf, []byte(`key1 = value1`))
require.NotNil(t, f)
require.NoError(t, err)
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `key1 = value1
[author]
E-MAIL = u@gogs.io
`,
buf.String(),
)
})
})
}
func Test_KeyValueDelimiters(t *testing.T) {
t.Run("custom key-value delimiters", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
KeyValueDelimiters: "?!",
}, []byte(`
[section]
key1?value1
key2!value2
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "value1", f.Section("section").Key("key1").String())
assert.Equal(t, "value2", f.Section("section").Key("key2").String())
})
}
func Test_PreserveSurroundedQuote(t *testing.T) {
t.Run("preserve surrounded quote test", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
PreserveSurroundedQuote: true,
}, []byte(`
[section]
key1 = "value1"
key2 = value2
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "\"value1\"", f.Section("section").Key("key1").String())
assert.Equal(t, "value2", f.Section("section").Key("key2").String())
})
t.Run("preserve surrounded quote test inverse test", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
PreserveSurroundedQuote: false,
}, []byte(`
[section]
key1 = "value1"
key2 = value2
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "value1", f.Section("section").Key("key1").String())
assert.Equal(t, "value2", f.Section("section").Key("key2").String())
})
}
type testData struct {
Value1 string `ini:"value1"`
Value2 string `ini:"value2"`
Value3 string `ini:"value3"`
}
func TestPythonMultiline(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")
}
path := filepath.Join("testdata", "multiline.ini")
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
ReaderBufferSize: 64 * 1024,
}, path)
require.NoError(t, err)
require.NotNil(t, f)
assert.Len(t, f.Sections(), 1)
defaultSection := f.Section("")
assert.NotNil(t, f.Section(""))
var testData testData
err = defaultSection.MapTo(&testData)
require.NoError(t, err)
assert.Equal(t, "some text here\n\tsome more text here\n\t\n\tthere is an empty line above and below\n\t", testData.Value1)
assert.Equal(t, "there is an empty line above\n that is not indented so it should not be part\n of the value", testData.Value2)
assert.Equal(t, `.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu consequat ac felis donec et odio pellentesque diam volutpat. Mauris commodo quis imperdiet massa tincidunt nunc. Interdum velit euismod in pellentesque. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Nascetur ridiculus mus mauris vitae. Posuere urna nec tincidunt praesent semper feugiat. Lorem donec massa sapien faucibus et molestie ac feugiat sed. Ipsum dolor sit amet consectetur adipiscing elit. Enim sed faucibus turpis in eu mi. A diam sollicitudin tempor id. Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit.
Lectus sit amet est placerat in egestas. At risus viverra adipiscing at in tellus integer. Tristique senectus et netus et malesuada fames ac. In hac habitasse platea dictumst. Purus in mollis nunc sed. Pellentesque sit amet porttitor eget dolor morbi. Elit at imperdiet dui accumsan sit amet nulla. Cursus in hac habitasse platea dictumst. Bibendum arcu vitae elementum curabitur. Faucibus ornare suspendisse sed nisi lacus. In vitae turpis massa sed. Libero nunc consequat interdum varius sit amet. Molestie a iaculis at erat pellentesque.
Dui faucibus in ornare quam viverra orci sagittis eu. Purus in mollis nunc sed id semper. Sed arcu non odio euismod lacinia at. Quis commodo odio aenean sed adipiscing diam donec. Quisque id diam vel quam elementum pulvinar. Lorem ipsum dolor sit amet. Purus ut faucibus pulvinar elementum integer enim neque volutpat ac. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh sed. Gravida rutrum quisque non tellus orci. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant. Et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Eget gravida cum sociis natoque penatibus et magnis. Elementum eu facilisis sed odio morbi quis commodo. Mollis nunc sed id semper risus in hendrerit gravida rutrum. Lorem dolor sed viverra ipsum.
Pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet. Justo eget magna fermentum iaculis eu non diam. Condimentum mattis pellentesque id nibh tortor id aliquet lectus. Tellus molestie nunc non blandit massa enim. Mauris ultrices eros in cursus turpis. Purus viverra accumsan in nisl nisi scelerisque. Quis lectus nulla at volutpat. Purus ut faucibus pulvinar elementum integer enim. In pellentesque massa placerat duis ultricies lacus sed turpis. Elit sed vulputate mi sit amet mauris commodo. Tellus elementum sagittis vitae et. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi nullam. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Libero id faucibus nisl tincidunt eget nullam. Mattis aliquam faucibus purus in massa tempor. Fames ac turpis egestas sed tempus urna. Gravida in fermentum et sollicitudin ac orci phasellus egestas.
Blandit turpis cursus in hac habitasse. Sed id semper risus in. Amet porttitor eget dolor morbi non arcu. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Ut morbi tincidunt augue interdum velit. Lorem mollis aliquam ut porttitor leo a. Nunc eget lorem dolor sed viverra. Scelerisque mauris pellentesque pulvinar pellentesque. Elit at imperdiet dui accumsan sit amet. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Laoreet non curabitur gravida arcu ac tortor dignissim. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Lacus sed viverra tellus in hac habitasse platea dictumst vestibulum. Viverra adipiscing at in tellus. Duis at tellus at urna condimentum. Eget gravida cum sociis natoque penatibus et magnis dis parturient. Pharetra massa massa ultricies mi quis hendrerit.
Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Maecenas volutpat blandit aliquam etiam. Sed turpis tincidunt id aliquet. Eget duis at tellus at urna condimentum. Pellentesque habitant morbi tristique senectus et. Amet aliquam id diam maecenas. Volutpat est velit egestas dui id. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel orci. Massa sed elementum tempus egestas sed sed risus pretium. Quam quisque id diam vel quam elementum pulvinar etiam non. Sapien faucibus et molestie ac. Ipsum dolor sit amet consectetur adipiscing. Viverra orci sagittis eu volutpat. Leo urna molestie at elementum. Commodo viverra maecenas accumsan lacus. Non sodales neque sodales ut etiam sit amet. Habitant morbi tristique senectus et netus et malesuada fames. Habitant morbi tristique senectus et netus et malesuada. Blandit aliquam etiam erat velit scelerisque in. Varius duis at consectetur lorem donec massa sapien faucibus et.
Augue mauris augue neque gravida in. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Nulla aliquet enim tortor at auctor urna nunc id. Morbi tristique senectus et netus et malesuada fames ac. Quam id leo in vitae turpis massa sed elementum tempus. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Maecenas volutpat blandit aliquam etiam erat velit scelerisque in. Sagittis nisl rhoncus mattis rhoncus urna neque viverra justo. Massa tempor nec feugiat nisl pretium. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum. Enim lobortis scelerisque fermentum dui faucibus in ornare. Faucibus ornare suspendisse sed nisi lacus. Morbi tristique senectus et netus et malesuada fames. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Dictum non consectetur a erat nam at. Leo urna molestie at elementum eu facilisis sed odio morbi. Quam id leo in vitae turpis massa. Neque egestas congue quisque egestas diam in arcu. Varius morbi enim nunc faucibus a pellentesque sit. Aliquet enim tortor at auctor urna.
Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Luctus accumsan tortor posuere ac. Eu ultrices vitae auctor eu augue ut lectus arcu bibendum. Pretium nibh ipsum consequat nisl vel pretium lectus. Aliquam etiam erat velit scelerisque in dictum. Sem et tortor consequat id porta nibh venenatis cras sed. A scelerisque purus semper eget duis at tellus at urna. At auctor urna nunc id. Ornare quam viverra orci sagittis eu volutpat odio. Nisl purus in mollis nunc sed id semper. Ornare suspendisse sed nisi lacus sed. Consectetur lorem donec massa sapien faucibus et. Ipsum dolor sit amet consectetur adipiscing elit ut. Porta nibh venenatis cras sed. Dignissim diam quis enim lobortis scelerisque. Quam nulla porttitor massa id. Tellus molestie nunc non blandit massa.
Malesuada fames ac turpis egestas. Suscipit tellus mauris a diam maecenas. Turpis in eu mi bibendum neque egestas. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Quis imperdiet massa tincidunt nunc pulvinar sapien et. Urna duis convallis convallis tellus id. Velit egestas dui id ornare arcu odio. Consectetur purus ut faucibus pulvinar elementum integer enim neque. Aenean sed adipiscing diam donec adipiscing tristique. Tortor aliquam nulla facilisi cras fermentum odio eu. Diam in arcu cursus euismod quis viverra nibh cras.
Id ornare arcu odio ut sem. Arcu dictum varius duis at consectetur lorem donec massa sapien. Proin libero nunc consequat interdum varius sit. Ut eu sem integer vitae justo. Vitae elementum curabitur vitae nunc. Diam quam nulla porttitor massa. Lectus mauris ultrices eros in cursus turpis massa tincidunt dui. Natoque penatibus et magnis dis parturient montes. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Libero nunc consequat interdum varius sit. Rhoncus dolor purus non enim praesent. Pellentesque sit amet porttitor eget. Nibh tortor id aliquet lectus proin nibh. Fermentum iaculis eu non diam phasellus vestibulum lorem sed.
Eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus. Habitant morbi tristique senectus et netus et malesuada fames ac. Urna condimentum mattis pellentesque id. Lorem sed risus ultricies tristique nulla aliquet enim tortor at. Ipsum dolor sit amet consectetur adipiscing elit. Convallis a cras semper auctor neque vitae tempus quam. A diam sollicitudin tempor id eu nisl nunc mi ipsum. Maecenas sed enim ut sem viverra aliquet eget. Massa enim nec dui nunc mattis enim. Nam aliquam sem et tortor consequat. Adipiscing commodo elit at imperdiet dui accumsan sit amet nulla. Nullam eget felis eget nunc lobortis. Mauris a diam maecenas sed enim ut sem viverra. Ornare massa eget egestas purus. In hac habitasse platea dictumst. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Nisl nunc mi ipsum faucibus. At varius vel pharetra vel. Mauris ultrices eros in cursus turpis massa tincidunt.`,
testData.Value3,
)
}
func TestPythonMultiline_EOF(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")
}
path := filepath.Join("testdata", "multiline_eof.ini")
f, err := LoadSources(LoadOptions{
AllowPythonMultilineValues: true,
ReaderBufferSize: 64 * 1024,
}, path)
require.NoError(t, err)
require.NotNil(t, f)
assert.Len(t, f.Sections(), 1)
defaultSection := f.Section("")
assert.NotNil(t, f.Section(""))
var testData testData
err = defaultSection.MapTo(&testData)
require.NoError(t, err)
assert.Equal(t, "some text here\n\tsome more text here 2", testData.Value1)
}
func Test_NestedValuesSpanningSections(t *testing.T) {
t.Run("basic nested value", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowNestedValues: true,
}, []byte(`
[section]
key1 = value1
key2 =
nested1 = nestedvalue1
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "value1", f.Section("section").Key("key1").String())
assert.Equal(t, "", f.Section("section").Key("key2").String())
assert.Equal(t, []string{"nested1 = nestedvalue1"}, f.Section("section").Key("key2").NestedValues())
})
t.Run("no nested values", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowNestedValues: true,
}, []byte(`
[section]
key1 = value1
key2 =
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "value1", f.Section("section").Key("key1").String())
assert.Equal(t, "", f.Section("section").Key("key2").String())
})
t.Run("no nested values and following sections", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowNestedValues: true,
}, []byte(`
[section]
key1 = value1
key2 =
[section2]
key3 = value3
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "value1", f.Section("section").Key("key1").String())
assert.Equal(t, "", f.Section("section").Key("key2").String())
assert.Equal(t, "value3", f.Section("section2").Key("key3").String())
})
t.Run("no nested values and following sections with indentation", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowNestedValues: true,
}, []byte(`
[section]
key1 = value1
key2 =
[section2]
key3 = value3
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "value1", f.Section("section").Key("key1").String())
assert.Equal(t, "", f.Section("section").Key("key2").String())
assert.Equal(t, "value3", f.Section("section2").Key("key3").String())
})
}
ini-1.67.0/key.go 0000664 0000000 0000000 00000057525 14274172402 0013524 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
// Key represents a key under a section.
type Key struct {
s *Section
Comment string
name string
value string
isAutoIncrement bool
isBooleanType bool
isShadow bool
shadows []*Key
nestedValues []string
}
// newKey simply return a key object with given values.
func newKey(s *Section, name, val string) *Key {
return &Key{
s: s,
name: name,
value: val,
}
}
func (k *Key) addShadow(val string) error {
if k.isShadow {
return errors.New("cannot add shadow to another shadow key")
} else if k.isAutoIncrement || k.isBooleanType {
return errors.New("cannot add shadow to auto-increment or boolean key")
}
if !k.s.f.options.AllowDuplicateShadowValues {
// Deduplicate shadows based on their values.
if k.value == val {
return nil
}
for i := range k.shadows {
if k.shadows[i].value == val {
return nil
}
}
}
shadow := newKey(k.s, k.name, val)
shadow.isShadow = true
k.shadows = append(k.shadows, shadow)
return nil
}
// AddShadow adds a new shadow key to itself.
func (k *Key) AddShadow(val string) error {
if !k.s.f.options.AllowShadows {
return errors.New("shadow key is not allowed")
}
return k.addShadow(val)
}
func (k *Key) addNestedValue(val string) error {
if k.isAutoIncrement || k.isBooleanType {
return errors.New("cannot add nested value to auto-increment or boolean key")
}
k.nestedValues = append(k.nestedValues, val)
return nil
}
// AddNestedValue adds a nested value to the key.
func (k *Key) AddNestedValue(val string) error {
if !k.s.f.options.AllowNestedValues {
return errors.New("nested value is not allowed")
}
return k.addNestedValue(val)
}
// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv
type ValueMapper func(string) string
// Name returns name of key.
func (k *Key) Name() string {
return k.name
}
// Value returns raw value of key for performance purpose.
func (k *Key) Value() string {
return k.value
}
// ValueWithShadows returns raw values of key and its shadows if any. Shadow
// keys with empty values are ignored from the returned list.
func (k *Key) ValueWithShadows() []string {
if len(k.shadows) == 0 {
if k.value == "" {
return []string{}
}
return []string{k.value}
}
vals := make([]string, 0, len(k.shadows)+1)
if k.value != "" {
vals = append(vals, k.value)
}
for _, s := range k.shadows {
if s.value != "" {
vals = append(vals, s.value)
}
}
return vals
}
// NestedValues returns nested values stored in the key.
// It is possible returned value is nil if no nested values stored in the key.
func (k *Key) NestedValues() []string {
return k.nestedValues
}
// transformValue takes a raw value and transforms to its final string.
func (k *Key) transformValue(val string) string {
if k.s.f.ValueMapper != nil {
val = k.s.f.ValueMapper(val)
}
// Fail-fast if no indicate char found for recursive value
if !strings.Contains(val, "%") {
return val
}
for i := 0; i < depthValues; i++ {
vr := varPattern.FindString(val)
if len(vr) == 0 {
break
}
// Take off leading '%(' and trailing ')s'.
noption := vr[2 : len(vr)-2]
// Search in the same section.
// If not found or found the key itself, then search again in default section.
nk, err := k.s.GetKey(noption)
if err != nil || k == nk {
nk, _ = k.s.f.Section("").GetKey(noption)
if nk == nil {
// Stop when no results found in the default section,
// and returns the value as-is.
break
}
}
// Substitute by new value and take off leading '%(' and trailing ')s'.
val = strings.Replace(val, vr, nk.value, -1)
}
return val
}
// String returns string representation of value.
func (k *Key) String() string {
return k.transformValue(k.value)
}
// Validate accepts a validate function which can
// return modifed result as key value.
func (k *Key) Validate(fn func(string) string) string {
return fn(k.String())
}
// parseBool returns the boolean value represented by the string.
//
// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On,
// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off.
// Any other value returns an error.
func parseBool(str string) (value bool, err error) {
switch str {
case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On":
return true, nil
case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off":
return false, nil
}
return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
}
// Bool returns bool type value.
func (k *Key) Bool() (bool, error) {
return parseBool(k.String())
}
// Float64 returns float64 type value.
func (k *Key) Float64() (float64, error) {
return strconv.ParseFloat(k.String(), 64)
}
// Int returns int type value.
func (k *Key) Int() (int, error) {
v, err := strconv.ParseInt(k.String(), 0, 64)
return int(v), err
}
// Int64 returns int64 type value.
func (k *Key) Int64() (int64, error) {
return strconv.ParseInt(k.String(), 0, 64)
}
// Uint returns uint type valued.
func (k *Key) Uint() (uint, error) {
u, e := strconv.ParseUint(k.String(), 0, 64)
return uint(u), e
}
// Uint64 returns uint64 type value.
func (k *Key) Uint64() (uint64, error) {
return strconv.ParseUint(k.String(), 0, 64)
}
// Duration returns time.Duration type value.
func (k *Key) Duration() (time.Duration, error) {
return time.ParseDuration(k.String())
}
// TimeFormat parses with given format and returns time.Time type value.
func (k *Key) TimeFormat(format string) (time.Time, error) {
return time.Parse(format, k.String())
}
// Time parses with RFC3339 format and returns time.Time type value.
func (k *Key) Time() (time.Time, error) {
return k.TimeFormat(time.RFC3339)
}
// MustString returns default value if key value is empty.
func (k *Key) MustString(defaultVal string) string {
val := k.String()
if len(val) == 0 {
k.value = defaultVal
return defaultVal
}
return val
}
// MustBool always returns value without error,
// it returns false if error occurs.
func (k *Key) MustBool(defaultVal ...bool) bool {
val, err := k.Bool()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatBool(defaultVal[0])
return defaultVal[0]
}
return val
}
// MustFloat64 always returns value without error,
// it returns 0.0 if error occurs.
func (k *Key) MustFloat64(defaultVal ...float64) float64 {
val, err := k.Float64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64)
return defaultVal[0]
}
return val
}
// MustInt always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustInt(defaultVal ...int) int {
val, err := k.Int()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatInt(int64(defaultVal[0]), 10)
return defaultVal[0]
}
return val
}
// MustInt64 always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustInt64(defaultVal ...int64) int64 {
val, err := k.Int64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatInt(defaultVal[0], 10)
return defaultVal[0]
}
return val
}
// MustUint always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustUint(defaultVal ...uint) uint {
val, err := k.Uint()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatUint(uint64(defaultVal[0]), 10)
return defaultVal[0]
}
return val
}
// MustUint64 always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustUint64(defaultVal ...uint64) uint64 {
val, err := k.Uint64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatUint(defaultVal[0], 10)
return defaultVal[0]
}
return val
}
// MustDuration always returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration {
val, err := k.Duration()
if len(defaultVal) > 0 && err != nil {
k.value = defaultVal[0].String()
return defaultVal[0]
}
return val
}
// MustTimeFormat always parses with given format and returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time {
val, err := k.TimeFormat(format)
if len(defaultVal) > 0 && err != nil {
k.value = defaultVal[0].Format(format)
return defaultVal[0]
}
return val
}
// MustTime always parses with RFC3339 format and returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustTime(defaultVal ...time.Time) time.Time {
return k.MustTimeFormat(time.RFC3339, defaultVal...)
}
// In always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) In(defaultVal string, candidates []string) string {
val := k.String()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InFloat64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 {
val := k.MustFloat64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InInt always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InInt(defaultVal int, candidates []int) int {
val := k.MustInt()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InInt64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 {
val := k.MustInt64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InUint always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InUint(defaultVal uint, candidates []uint) uint {
val := k.MustUint()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InUint64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 {
val := k.MustUint64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InTimeFormat always parses with given format and returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time {
val := k.MustTimeFormat(format)
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InTime always parses with RFC3339 format and returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
}
// RangeFloat64 checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
val := k.MustFloat64()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeInt checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeInt(defaultVal, min, max int) int {
val := k.MustInt()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeInt64 checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
val := k.MustInt64()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeTimeFormat checks if value with given format is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
val := k.MustTimeFormat(format)
if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
return defaultVal
}
return val
}
// RangeTime checks if value with RFC3339 format is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
}
// Strings returns list of string divided by given delimiter.
func (k *Key) Strings(delim string) []string {
str := k.String()
if len(str) == 0 {
return []string{}
}
runes := []rune(str)
vals := make([]string, 0, 2)
var buf bytes.Buffer
escape := false
idx := 0
for {
if escape {
escape = false
if runes[idx] != '\\' && !strings.HasPrefix(string(runes[idx:]), delim) {
buf.WriteRune('\\')
}
buf.WriteRune(runes[idx])
} else {
if runes[idx] == '\\' {
escape = true
} else if strings.HasPrefix(string(runes[idx:]), delim) {
idx += len(delim) - 1
vals = append(vals, strings.TrimSpace(buf.String()))
buf.Reset()
} else {
buf.WriteRune(runes[idx])
}
}
idx++
if idx == len(runes) {
break
}
}
if buf.Len() > 0 {
vals = append(vals, strings.TrimSpace(buf.String()))
}
return vals
}
// StringsWithShadows returns list of string divided by given delimiter.
// Shadows will also be appended if any.
func (k *Key) StringsWithShadows(delim string) []string {
vals := k.ValueWithShadows()
results := make([]string, 0, len(vals)*2)
for i := range vals {
if len(vals) == 0 {
continue
}
results = append(results, strings.Split(vals[i], delim)...)
}
for i := range results {
results[i] = k.transformValue(strings.TrimSpace(results[i]))
}
return results
}
// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Float64s(delim string) []float64 {
vals, _ := k.parseFloat64s(k.Strings(delim), true, false)
return vals
}
// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Ints(delim string) []int {
vals, _ := k.parseInts(k.Strings(delim), true, false)
return vals
}
// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Int64s(delim string) []int64 {
vals, _ := k.parseInt64s(k.Strings(delim), true, false)
return vals
}
// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Uints(delim string) []uint {
vals, _ := k.parseUints(k.Strings(delim), true, false)
return vals
}
// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Uint64s(delim string) []uint64 {
vals, _ := k.parseUint64s(k.Strings(delim), true, false)
return vals
}
// Bools returns list of bool divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Bools(delim string) []bool {
vals, _ := k.parseBools(k.Strings(delim), true, false)
return vals
}
// TimesFormat parses with given format and returns list of time.Time divided by given delimiter.
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
func (k *Key) TimesFormat(format, delim string) []time.Time {
vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false)
return vals
}
// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter.
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
func (k *Key) Times(delim string) []time.Time {
return k.TimesFormat(time.RFC3339, delim)
}
// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then
// it will not be included to result list.
func (k *Key) ValidFloat64s(delim string) []float64 {
vals, _ := k.parseFloat64s(k.Strings(delim), false, false)
return vals
}
// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will
// not be included to result list.
func (k *Key) ValidInts(delim string) []int {
vals, _ := k.parseInts(k.Strings(delim), false, false)
return vals
}
// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer,
// then it will not be included to result list.
func (k *Key) ValidInt64s(delim string) []int64 {
vals, _ := k.parseInt64s(k.Strings(delim), false, false)
return vals
}
// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer,
// then it will not be included to result list.
func (k *Key) ValidUints(delim string) []uint {
vals, _ := k.parseUints(k.Strings(delim), false, false)
return vals
}
// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned
// integer, then it will not be included to result list.
func (k *Key) ValidUint64s(delim string) []uint64 {
vals, _ := k.parseUint64s(k.Strings(delim), false, false)
return vals
}
// ValidBools returns list of bool divided by given delimiter. If some value is not 64-bit unsigned
// integer, then it will not be included to result list.
func (k *Key) ValidBools(delim string) []bool {
vals, _ := k.parseBools(k.Strings(delim), false, false)
return vals
}
// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
func (k *Key) ValidTimesFormat(format, delim string) []time.Time {
vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false)
return vals
}
// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter.
func (k *Key) ValidTimes(delim string) []time.Time {
return k.ValidTimesFormat(time.RFC3339, delim)
}
// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictFloat64s(delim string) ([]float64, error) {
return k.parseFloat64s(k.Strings(delim), false, true)
}
// StrictInts returns list of int divided by given delimiter or error on first invalid input.
func (k *Key) StrictInts(delim string) ([]int, error) {
return k.parseInts(k.Strings(delim), false, true)
}
// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictInt64s(delim string) ([]int64, error) {
return k.parseInt64s(k.Strings(delim), false, true)
}
// StrictUints returns list of uint divided by given delimiter or error on first invalid input.
func (k *Key) StrictUints(delim string) ([]uint, error) {
return k.parseUints(k.Strings(delim), false, true)
}
// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictUint64s(delim string) ([]uint64, error) {
return k.parseUint64s(k.Strings(delim), false, true)
}
// StrictBools returns list of bool divided by given delimiter or error on first invalid input.
func (k *Key) StrictBools(delim string) ([]bool, error) {
return k.parseBools(k.Strings(delim), false, true)
}
// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter
// or error on first invalid input.
func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) {
return k.parseTimesFormat(format, k.Strings(delim), false, true)
}
// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter
// or error on first invalid input.
func (k *Key) StrictTimes(delim string) ([]time.Time, error) {
return k.StrictTimesFormat(time.RFC3339, delim)
}
// parseBools transforms strings to bools.
func (k *Key) parseBools(strs []string, addInvalid, returnOnInvalid bool) ([]bool, error) {
vals := make([]bool, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := parseBool(str)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(bool))
}
}
return vals, err
}
// parseFloat64s transforms strings to float64s.
func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) {
vals := make([]float64, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseFloat(str, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(float64))
}
}
return vals, err
}
// parseInts transforms strings to ints.
func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) {
vals := make([]int, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseInt(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, int(val.(int64)))
}
}
return vals, err
}
// parseInt64s transforms strings to int64s.
func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) {
vals := make([]int64, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseInt(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(int64))
}
}
return vals, err
}
// parseUints transforms strings to uints.
func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) {
vals := make([]uint, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseUint(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, uint(val.(uint64)))
}
}
return vals, err
}
// parseUint64s transforms strings to uint64s.
func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
vals := make([]uint64, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseUint(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(uint64))
}
}
return vals, err
}
type Parser func(str string) (interface{}, error)
// parseTimesFormat transforms strings to times in given format.
func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) {
vals := make([]time.Time, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := time.Parse(format, str)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(time.Time))
}
}
return vals, err
}
// doParse transforms strings to different types
func (k *Key) doParse(strs []string, addInvalid, returnOnInvalid bool, parser Parser) ([]interface{}, error) {
vals := make([]interface{}, 0, len(strs))
for _, str := range strs {
val, err := parser(str)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, val)
}
}
return vals, nil
}
// SetValue changes key value.
func (k *Key) SetValue(v string) {
if k.s.f.BlockMode {
k.s.f.lock.Lock()
defer k.s.f.lock.Unlock()
}
k.value = v
k.s.keysHash[k.name] = v
}
ini-1.67.0/key_test.go 0000664 0000000 0000000 00000047472 14274172402 0014563 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"fmt"
"runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKey_AddShadow(t *testing.T) {
t.Run("add shadow to a key", func(t *testing.T) {
f, err := ShadowLoad([]byte(`
[notes]
-: note1`))
require.NoError(t, err)
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.NoError(t, k.AddShadow("ini.v1"))
assert.Equal(t, []string{"ini", "ini.v1"}, k.ValueWithShadows())
t.Run("add shadow to boolean key", func(t *testing.T) {
k, err := f.Section("").NewBooleanKey("published")
require.NoError(t, err)
require.NotNil(t, k)
assert.Error(t, k.AddShadow("beta"))
})
t.Run("add shadow to auto-increment key", func(t *testing.T) {
assert.Error(t, f.Section("notes").Key("#1").AddShadow("beta"))
})
t.Run("deduplicate an existing value", func(t *testing.T) {
k := f.Section("").Key("NAME")
assert.NoError(t, k.AddShadow("ini"))
assert.Equal(t, []string{"ini", "ini.v1"}, k.ValueWithShadows())
})
t.Run("ignore empty shadow values", func(t *testing.T) {
k := f.Section("").Key("empty")
assert.NoError(t, k.AddShadow(""))
assert.NoError(t, k.AddShadow("ini"))
assert.Equal(t, []string{"ini"}, k.ValueWithShadows())
})
})
t.Run("allow duplicate shadowed values", func(t *testing.T) {
f := Empty(LoadOptions{
AllowShadows: true,
AllowDuplicateShadowValues: true,
})
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.NoError(t, k.AddShadow("ini.v1"))
assert.NoError(t, k.AddShadow("ini"))
assert.NoError(t, k.AddShadow("ini"))
assert.Equal(t, []string{"ini", "ini.v1", "ini", "ini"}, k.ValueWithShadows())
})
t.Run("shadow is not allowed", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.Error(t, k.AddShadow("ini.v1"))
})
}
// Helpers for slice tests.
func float64sEqual(t *testing.T, values []float64, expected ...float64) {
t.Helper()
assert.Len(t, values, len(expected))
for i, v := range expected {
assert.Equal(t, v, values[i])
}
}
func intsEqual(t *testing.T, values []int, expected ...int) {
t.Helper()
assert.Len(t, values, len(expected))
for i, v := range expected {
assert.Equal(t, v, values[i])
}
}
func int64sEqual(t *testing.T, values []int64, expected ...int64) {
t.Helper()
assert.Len(t, values, len(expected))
for i, v := range expected {
assert.Equal(t, v, values[i])
}
}
func uintsEqual(t *testing.T, values []uint, expected ...uint) {
t.Helper()
assert.Len(t, values, len(expected))
for i, v := range expected {
assert.Equal(t, v, values[i])
}
}
func uint64sEqual(t *testing.T, values []uint64, expected ...uint64) {
t.Helper()
assert.Len(t, values, len(expected))
for i, v := range expected {
assert.Equal(t, v, values[i])
}
}
func boolsEqual(t *testing.T, values []bool, expected ...bool) {
t.Helper()
assert.Len(t, values, len(expected))
for i, v := range expected {
assert.Equal(t, v, values[i])
}
}
func timesEqual(t *testing.T, values []time.Time, expected ...time.Time) {
t.Helper()
assert.Len(t, values, len(expected))
for i, v := range expected {
assert.Equal(t, v.String(), values[i].String())
}
}
func TestKey_Helpers(t *testing.T) {
t.Run("getting and setting values", func(t *testing.T) {
f, err := Load(fullConf)
require.NoError(t, err)
require.NotNil(t, f)
t.Run("get string representation", func(t *testing.T) {
sec := f.Section("")
require.NotNil(t, sec)
assert.Equal(t, "ini", sec.Key("NAME").Value())
assert.Equal(t, "ini", sec.Key("NAME").String())
assert.Equal(t, "ini", sec.Key("NAME").Validate(func(in string) string {
return in
}))
assert.Equal(t, "; Package name", sec.Key("NAME").Comment)
assert.Equal(t, "gopkg.in/ini.v1", sec.Key("IMPORT_PATH").String())
t.Run("with ValueMapper", func(t *testing.T) {
f.ValueMapper = func(in string) string {
if in == "gopkg.in/%(NAME)s.%(VERSION)s" {
return "github.com/go-ini/ini"
}
return in
}
assert.Equal(t, "github.com/go-ini/ini", sec.Key("IMPORT_PATH").String())
})
})
t.Run("get values in non-default section", func(t *testing.T) {
sec := f.Section("author")
require.NotNil(t, sec)
assert.Equal(t, "Unknwon", sec.Key("NAME").String())
assert.Equal(t, "https://github.com/Unknwon", sec.Key("GITHUB").String())
sec = f.Section("package")
require.NotNil(t, sec)
assert.Equal(t, "https://gopkg.in/ini.v1", sec.Key("CLONE_URL").String())
})
t.Run("get auto-increment key names", func(t *testing.T) {
keys := f.Section("features").Keys()
for i, k := range keys {
assert.Equal(t, fmt.Sprintf("#%d", i+1), k.Name())
}
})
t.Run("get parent-keys that are available to the child section", func(t *testing.T) {
parentKeys := f.Section("package.sub").ParentKeys()
for _, k := range parentKeys {
assert.Equal(t, "CLONE_URL", k.Name())
}
})
t.Run("get overwrite value", func(t *testing.T) {
assert.Equal(t, "u@gogs.io", f.Section("author").Key("E-MAIL").String())
})
t.Run("get sections", func(t *testing.T) {
sections := f.Sections()
for i, name := range []string{DefaultSection, "author", "package", "package.sub", "features", "types", "array", "note", "comments", "string escapes", "advance"} {
assert.Equal(t, name, sections[i].Name())
}
})
t.Run("get parent section value", func(t *testing.T) {
assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package.sub").Key("CLONE_URL").String())
assert.Equal(t, "https://gopkg.in/ini.v1", f.Section("package.fake.sub").Key("CLONE_URL").String())
})
t.Run("get multiple line value", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Skipping testing on Windows")
}
assert.Equal(t, "Gopher.\nCoding addict.\nGood man.\n", f.Section("author").Key("BIO").String())
})
t.Run("get values with type", func(t *testing.T) {
sec := f.Section("types")
v1, err := sec.Key("BOOL").Bool()
require.NoError(t, err)
assert.True(t, v1)
v1, err = sec.Key("BOOL_FALSE").Bool()
require.NoError(t, err)
assert.False(t, v1)
v2, err := sec.Key("FLOAT64").Float64()
require.NoError(t, err)
assert.Equal(t, 1.25, v2)
v3, err := sec.Key("INT").Int()
require.NoError(t, err)
assert.Equal(t, 10, v3)
v4, err := sec.Key("INT").Int64()
require.NoError(t, err)
assert.Equal(t, int64(10), v4)
v5, err := sec.Key("UINT").Uint()
require.NoError(t, err)
assert.Equal(t, uint(3), v5)
v6, err := sec.Key("UINT").Uint64()
require.NoError(t, err)
assert.Equal(t, uint64(3), v6)
ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
require.NoError(t, err)
v7, err := sec.Key("TIME").Time()
require.NoError(t, err)
assert.Equal(t, ti.String(), v7.String())
v8, err := sec.Key("HEX_NUMBER").Int()
require.NoError(t, err)
assert.Equal(t, 0x3000, v8)
t.Run("must get values with type", func(t *testing.T) {
assert.Equal(t, "str", sec.Key("STRING").MustString("404"))
assert.True(t, sec.Key("BOOL").MustBool())
assert.Equal(t, float64(1.25), sec.Key("FLOAT64").MustFloat64())
assert.Equal(t, int(10), sec.Key("INT").MustInt())
assert.Equal(t, int64(10), sec.Key("INT").MustInt64())
assert.Equal(t, uint(3), sec.Key("UINT").MustUint())
assert.Equal(t, uint64(3), sec.Key("UINT").MustUint64())
assert.Equal(t, ti.String(), sec.Key("TIME").MustTime().String())
assert.Equal(t, 0x3000, sec.Key("HEX_NUMBER").MustInt())
dur, err := time.ParseDuration("2h45m")
require.NoError(t, err)
assert.Equal(t, dur.Seconds(), sec.Key("DURATION").MustDuration().Seconds())
t.Run("must get values with default value", func(t *testing.T) {
assert.Equal(t, "404", sec.Key("STRING_404").MustString("404"))
assert.True(t, sec.Key("BOOL_404").MustBool(true))
assert.Equal(t, float64(2.5), sec.Key("FLOAT64_404").MustFloat64(2.5))
assert.Equal(t, int(15), sec.Key("INT_404").MustInt(15))
assert.Equal(t, int64(15), sec.Key("INT64_404").MustInt64(15))
assert.Equal(t, uint(6), sec.Key("UINT_404").MustUint(6))
assert.Equal(t, uint64(6), sec.Key("UINT64_404").MustUint64(6))
assert.Equal(t, 0x3001, sec.Key("HEX_NUMBER_404").MustInt(0x3001))
ti, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z")
require.NoError(t, err)
assert.Equal(t, ti.String(), sec.Key("TIME_404").MustTime(ti).String())
assert.Equal(t, dur.Seconds(), sec.Key("DURATION_404").MustDuration(dur).Seconds())
t.Run("must should set default as key value", func(t *testing.T) {
assert.Equal(t, "404", sec.Key("STRING_404").String())
assert.Equal(t, "true", sec.Key("BOOL_404").String())
assert.Equal(t, "2.5", sec.Key("FLOAT64_404").String())
assert.Equal(t, "15", sec.Key("INT_404").String())
assert.Equal(t, "15", sec.Key("INT64_404").String())
assert.Equal(t, "6", sec.Key("UINT_404").String())
assert.Equal(t, "6", sec.Key("UINT64_404").String())
assert.Equal(t, "2014-01-01T20:17:05Z", sec.Key("TIME_404").String())
assert.Equal(t, "2h45m0s", sec.Key("DURATION_404").String())
assert.Equal(t, "12289", sec.Key("HEX_NUMBER_404").String())
})
})
})
})
t.Run("get value with candidates", func(t *testing.T) {
sec := f.Section("types")
assert.Equal(t, "str", sec.Key("STRING").In("", []string{"str", "arr", "types"}))
assert.Equal(t, float64(1.25), sec.Key("FLOAT64").InFloat64(0, []float64{1.25, 2.5, 3.75}))
assert.Equal(t, int(10), sec.Key("INT").InInt(0, []int{10, 20, 30}))
assert.Equal(t, int64(10), sec.Key("INT").InInt64(0, []int64{10, 20, 30}))
assert.Equal(t, uint(3), sec.Key("UINT").InUint(0, []uint{3, 6, 9}))
assert.Equal(t, uint64(3), sec.Key("UINT").InUint64(0, []uint64{3, 6, 9}))
zt, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
require.NoError(t, err)
ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
require.NoError(t, err)
assert.Equal(t, ti.String(), sec.Key("TIME").InTime(zt, []time.Time{ti, time.Now(), time.Now().Add(1 * time.Second)}).String())
t.Run("get value with candidates and default value", func(t *testing.T) {
assert.Equal(t, "str", sec.Key("STRING_404_2").In("str", []string{"str", "arr", "types"}))
assert.Equal(t, float64(1.25), sec.Key("FLOAT64_404_2").InFloat64(1.25, []float64{1.25, 2.5, 3.75}))
assert.Equal(t, int(10), sec.Key("INT_404_2").InInt(10, []int{10, 20, 30}))
assert.Equal(t, int64(10), sec.Key("INT64_404_2").InInt64(10, []int64{10, 20, 30}))
assert.Equal(t, uint(3), sec.Key("UINT_404_2").InUint(3, []uint{3, 6, 9}))
assert.Equal(t, uint64(3), sec.Key("UINT_404_2").InUint64(3, []uint64{3, 6, 9}))
assert.Equal(t, ti.String(), sec.Key("TIME_404_2").InTime(ti, []time.Time{time.Now(), time.Now(), time.Now().Add(1 * time.Second)}).String())
})
})
t.Run("get values in range", func(t *testing.T) {
sec := f.Section("types")
assert.Equal(t, float64(1.25), sec.Key("FLOAT64").RangeFloat64(0, 1, 2))
assert.Equal(t, int(10), sec.Key("INT").RangeInt(0, 10, 20))
assert.Equal(t, int64(10), sec.Key("INT").RangeInt64(0, 10, 20))
minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
require.NoError(t, err)
midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z")
require.NoError(t, err)
maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z")
require.NoError(t, err)
ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
require.NoError(t, err)
assert.Equal(t, ti.String(), sec.Key("TIME").RangeTime(ti, minT, maxT).String())
t.Run("get value in range with default value", func(t *testing.T) {
assert.Equal(t, float64(5), sec.Key("FLOAT64").RangeFloat64(5, 0, 1))
assert.Equal(t, 7, sec.Key("INT").RangeInt(7, 0, 5))
assert.Equal(t, int64(7), sec.Key("INT").RangeInt64(7, 0, 5))
assert.Equal(t, ti.String(), sec.Key("TIME").RangeTime(ti, minT, midT).String())
})
})
t.Run("get values into slice", func(t *testing.T) {
sec := f.Section("array")
assert.Equal(t, "en,zh,de", strings.Join(sec.Key("STRINGS").Strings(","), ","))
assert.Equal(t, 0, len(sec.Key("STRINGS_404").Strings(",")))
vals1 := sec.Key("FLOAT64S").Float64s(",")
float64sEqual(t, vals1, 1.1, 2.2, 3.3)
vals2 := sec.Key("INTS").Ints(",")
intsEqual(t, vals2, 1, 2, 3)
vals3 := sec.Key("INTS").Int64s(",")
int64sEqual(t, vals3, 1, 2, 3)
vals4 := sec.Key("UINTS").Uints(",")
uintsEqual(t, vals4, 1, 2, 3)
vals5 := sec.Key("UINTS").Uint64s(",")
uint64sEqual(t, vals5, 1, 2, 3)
vals6 := sec.Key("BOOLS").Bools(",")
boolsEqual(t, vals6, true, false, false)
ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
require.NoError(t, err)
vals7 := sec.Key("TIMES").Times(",")
timesEqual(t, vals7, ti, ti, ti)
})
t.Run("test string slice escapes", func(t *testing.T) {
sec := f.Section("string escapes")
assert.Equal(t, []string{"value1", "value2", "value3"}, sec.Key("key1").Strings(","))
assert.Equal(t, []string{"value1, value2"}, sec.Key("key2").Strings(","))
assert.Equal(t, []string{`val\ue1`, "value2"}, sec.Key("key3").Strings(","))
assert.Equal(t, []string{`value1\`, `value\\2`}, sec.Key("key4").Strings(","))
assert.Equal(t, []string{"value1,, value2"}, sec.Key("key5").Strings(",,"))
assert.Equal(t, []string{"aaa", "bbb and space", "ccc"}, sec.Key("key6").Strings(" "))
})
t.Run("get valid values into slice", func(t *testing.T) {
sec := f.Section("array")
vals1 := sec.Key("FLOAT64S").ValidFloat64s(",")
float64sEqual(t, vals1, 1.1, 2.2, 3.3)
vals2 := sec.Key("INTS").ValidInts(",")
intsEqual(t, vals2, 1, 2, 3)
vals3 := sec.Key("INTS").ValidInt64s(",")
int64sEqual(t, vals3, 1, 2, 3)
vals4 := sec.Key("UINTS").ValidUints(",")
uintsEqual(t, vals4, 1, 2, 3)
vals5 := sec.Key("UINTS").ValidUint64s(",")
uint64sEqual(t, vals5, 1, 2, 3)
vals6 := sec.Key("BOOLS").ValidBools(",")
boolsEqual(t, vals6, true, false, false)
ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
require.NoError(t, err)
vals7 := sec.Key("TIMES").ValidTimes(",")
timesEqual(t, vals7, ti, ti, ti)
})
t.Run("get values one type into slice of another type", func(t *testing.T) {
sec := f.Section("array")
vals1 := sec.Key("STRINGS").ValidFloat64s(",")
assert.Empty(t, vals1)
vals2 := sec.Key("STRINGS").ValidInts(",")
assert.Empty(t, vals2)
vals3 := sec.Key("STRINGS").ValidInt64s(",")
assert.Empty(t, vals3)
vals4 := sec.Key("STRINGS").ValidUints(",")
assert.Empty(t, vals4)
vals5 := sec.Key("STRINGS").ValidUint64s(",")
assert.Empty(t, vals5)
vals6 := sec.Key("STRINGS").ValidBools(",")
assert.Empty(t, vals6)
vals7 := sec.Key("STRINGS").ValidTimes(",")
assert.Empty(t, vals7)
})
t.Run("get valid values into slice without errors", func(t *testing.T) {
sec := f.Section("array")
vals1, err := sec.Key("FLOAT64S").StrictFloat64s(",")
require.NoError(t, err)
float64sEqual(t, vals1, 1.1, 2.2, 3.3)
vals2, err := sec.Key("INTS").StrictInts(",")
require.NoError(t, err)
intsEqual(t, vals2, 1, 2, 3)
vals3, err := sec.Key("INTS").StrictInt64s(",")
require.NoError(t, err)
int64sEqual(t, vals3, 1, 2, 3)
vals4, err := sec.Key("UINTS").StrictUints(",")
require.NoError(t, err)
uintsEqual(t, vals4, 1, 2, 3)
vals5, err := sec.Key("UINTS").StrictUint64s(",")
require.NoError(t, err)
uint64sEqual(t, vals5, 1, 2, 3)
vals6, err := sec.Key("BOOLS").StrictBools(",")
require.NoError(t, err)
boolsEqual(t, vals6, true, false, false)
ti, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
require.NoError(t, err)
vals7, err := sec.Key("TIMES").StrictTimes(",")
require.NoError(t, err)
timesEqual(t, vals7, ti, ti, ti)
})
t.Run("get invalid values into slice", func(t *testing.T) {
sec := f.Section("array")
vals1, err := sec.Key("STRINGS").StrictFloat64s(",")
assert.Empty(t, vals1)
assert.Error(t, err)
vals2, err := sec.Key("STRINGS").StrictInts(",")
assert.Empty(t, vals2)
assert.Error(t, err)
vals3, err := sec.Key("STRINGS").StrictInt64s(",")
assert.Empty(t, vals3)
assert.Error(t, err)
vals4, err := sec.Key("STRINGS").StrictUints(",")
assert.Empty(t, vals4)
assert.Error(t, err)
vals5, err := sec.Key("STRINGS").StrictUint64s(",")
assert.Empty(t, vals5)
assert.Error(t, err)
vals6, err := sec.Key("STRINGS").StrictBools(",")
assert.Empty(t, vals6)
assert.Error(t, err)
vals7, err := sec.Key("STRINGS").StrictTimes(",")
assert.Empty(t, vals7)
assert.Error(t, err)
})
})
}
func TestKey_ValueWithShadows(t *testing.T) {
t.Run("", func(t *testing.T) {
f, err := ShadowLoad([]byte(`
keyName = value1
keyName = value2
`))
require.NoError(t, err)
require.NotNil(t, f)
k := f.Section("").Key("FakeKey")
require.NotNil(t, k)
assert.Equal(t, []string{}, k.ValueWithShadows())
k = f.Section("").Key("keyName")
require.NotNil(t, k)
assert.Equal(t, []string{"value1", "value2"}, k.ValueWithShadows())
})
}
func TestKey_StringsWithShadows(t *testing.T) {
t.Run("get strings of shadows of a key", func(t *testing.T) {
f, err := ShadowLoad([]byte(""))
require.NoError(t, err)
require.NotNil(t, f)
k, err := f.Section("").NewKey("NUMS", "1,2")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("").NewKey("NUMS", "4,5,6")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, []string{"1", "2", "4", "5", "6"}, k.StringsWithShadows(","))
})
}
func TestKey_SetValue(t *testing.T) {
t.Run("set value of key", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, "ini", k.Value())
k.SetValue("ini.v1")
assert.Equal(t, "ini.v1", k.Value())
})
}
func TestKey_NestedValues(t *testing.T) {
t.Run("read and write nested values", func(t *testing.T) {
f, err := LoadSources(LoadOptions{
AllowNestedValues: true,
}, []byte(`
aws_access_key_id = foo
aws_secret_access_key = bar
region = us-west-2
s3 =
max_concurrent_requests=10
max_queue_size=1000`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, []string{"max_concurrent_requests=10", "max_queue_size=1000"}, f.Section("").Key("s3").NestedValues())
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `aws_access_key_id = foo
aws_secret_access_key = bar
region = us-west-2
s3 =
max_concurrent_requests=10
max_queue_size=1000
`,
buf.String(),
)
})
}
func TestRecursiveValues(t *testing.T) {
t.Run("recursive values should not reflect on same key", func(t *testing.T) {
f, err := Load([]byte(`
NAME = ini
expires = yes
[package]
NAME = %(NAME)s
expires = %(expires)s`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "ini", f.Section("package").Key("NAME").String())
assert.Equal(t, "yes", f.Section("package").Key("expires").String())
})
t.Run("recursive value with no target found", func(t *testing.T) {
f, err := Load([]byte(`
[foo]
bar = %(missing)s
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "%(missing)s", f.Section("foo").Key("bar").String())
})
}
ini-1.67.0/parser.go 0000664 0000000 0000000 00000031404 14274172402 0014214 0 ustar 00root root 0000000 0000000 // Copyright 2015 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"unicode"
)
const minReaderBufferSize = 4096
var pythonMultiline = regexp.MustCompile(`^([\t\f ]+)(.*)`)
type parserOptions struct {
IgnoreContinuation bool
IgnoreInlineComment bool
AllowPythonMultilineValues bool
SpaceBeforeInlineComment bool
UnescapeValueDoubleQuotes bool
UnescapeValueCommentSymbols bool
PreserveSurroundedQuote bool
DebugFunc DebugFunc
ReaderBufferSize int
}
type parser struct {
buf *bufio.Reader
options parserOptions
isEOF bool
count int
comment *bytes.Buffer
}
func (p *parser) debug(format string, args ...interface{}) {
if p.options.DebugFunc != nil {
p.options.DebugFunc(fmt.Sprintf(format, args...))
}
}
func newParser(r io.Reader, opts parserOptions) *parser {
size := opts.ReaderBufferSize
if size < minReaderBufferSize {
size = minReaderBufferSize
}
return &parser{
buf: bufio.NewReaderSize(r, size),
options: opts,
count: 1,
comment: &bytes.Buffer{},
}
}
// BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format.
// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
func (p *parser) BOM() error {
mask, err := p.buf.Peek(2)
if err != nil && err != io.EOF {
return err
} else if len(mask) < 2 {
return nil
}
switch {
case mask[0] == 254 && mask[1] == 255:
fallthrough
case mask[0] == 255 && mask[1] == 254:
_, err = p.buf.Read(mask)
if err != nil {
return err
}
case mask[0] == 239 && mask[1] == 187:
mask, err := p.buf.Peek(3)
if err != nil && err != io.EOF {
return err
} else if len(mask) < 3 {
return nil
}
if mask[2] == 191 {
_, err = p.buf.Read(mask)
if err != nil {
return err
}
}
}
return nil
}
func (p *parser) readUntil(delim byte) ([]byte, error) {
data, err := p.buf.ReadBytes(delim)
if err != nil {
if err == io.EOF {
p.isEOF = true
} else {
return nil, err
}
}
return data, nil
}
func cleanComment(in []byte) ([]byte, bool) {
i := bytes.IndexAny(in, "#;")
if i == -1 {
return nil, false
}
return in[i:], true
}
func readKeyName(delimiters string, in []byte) (string, int, error) {
line := string(in)
// Check if key name surrounded by quotes.
var keyQuote string
if line[0] == '"' {
if len(line) > 6 && line[0:3] == `"""` {
keyQuote = `"""`
} else {
keyQuote = `"`
}
} else if line[0] == '`' {
keyQuote = "`"
}
// Get out key name
var endIdx int
if len(keyQuote) > 0 {
startIdx := len(keyQuote)
// FIXME: fail case -> """"""name"""=value
pos := strings.Index(line[startIdx:], keyQuote)
if pos == -1 {
return "", -1, fmt.Errorf("missing closing key quote: %s", line)
}
pos += startIdx
// Find key-value delimiter
i := strings.IndexAny(line[pos+startIdx:], delimiters)
if i < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
endIdx = pos + i
return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
}
endIdx = strings.IndexAny(line, delimiters)
if endIdx < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
if endIdx == 0 {
return "", -1, ErrEmptyKeyName{line}
}
return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil
}
func (p *parser) readMultilines(line, val, valQuote string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := string(data)
pos := strings.LastIndex(next, valQuote)
if pos > -1 {
val += next[:pos]
comment, has := cleanComment([]byte(next[pos:]))
if has {
p.comment.Write(bytes.TrimSpace(comment))
}
break
}
val += next
if p.isEOF {
return "", fmt.Errorf("missing closing key quote from %q to %q", line, next)
}
}
return val, nil
}
func (p *parser) readContinuationLines(val string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := strings.TrimSpace(string(data))
if len(next) == 0 {
break
}
val += next
if val[len(val)-1] != '\\' {
break
}
val = val[:len(val)-1]
}
return val, nil
}
// hasSurroundedQuote check if and only if the first and last characters
// are quotes \" or \'.
// It returns false if any other parts also contain same kind of quotes.
func hasSurroundedQuote(in string, quote byte) bool {
return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote &&
strings.IndexByte(in[1:], quote) == len(in)-2
}
func (p *parser) readValue(in []byte, bufferSize int) (string, error) {
line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
if len(line) == 0 {
if p.options.AllowPythonMultilineValues && len(in) > 0 && in[len(in)-1] == '\n' {
return p.readPythonMultilines(line, bufferSize)
}
return "", nil
}
var valQuote string
if len(line) > 3 && line[0:3] == `"""` {
valQuote = `"""`
} else if line[0] == '`' {
valQuote = "`"
} else if p.options.UnescapeValueDoubleQuotes && line[0] == '"' {
valQuote = `"`
}
if len(valQuote) > 0 {
startIdx := len(valQuote)
pos := strings.LastIndex(line[startIdx:], valQuote)
// Check for multi-line value
if pos == -1 {
return p.readMultilines(line, line[startIdx:], valQuote)
}
if p.options.UnescapeValueDoubleQuotes && valQuote == `"` {
return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil
}
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 !p.options.IgnoreContinuation && trimmedLastChar == '\\' {
return p.readContinuationLines(line[:len(line)-1])
}
// Check if ignore inline comment
if !p.options.IgnoreInlineComment {
var i int
if p.options.SpaceBeforeInlineComment {
i = strings.Index(line, " #")
if i == -1 {
i = strings.Index(line, " ;")
}
} else {
i = strings.IndexAny(line, "#;")
}
if i > -1 {
p.comment.WriteString(line[i:])
line = strings.TrimSpace(line[:i])
}
}
// Trim single and double quotes
if (hasSurroundedQuote(line, '\'') ||
hasSurroundedQuote(line, '"')) && !p.options.PreserveSurroundedQuote {
line = line[1 : len(line)-1]
} else if len(valQuote) == 0 && p.options.UnescapeValueCommentSymbols {
line = strings.ReplaceAll(line, `\;`, ";")
line = strings.ReplaceAll(line, `\#`, "#")
} else if p.options.AllowPythonMultilineValues && lastChar == '\n' {
return p.readPythonMultilines(line, bufferSize)
}
return line, nil
}
func (p *parser) readPythonMultilines(line string, bufferSize int) (string, error) {
parserBufferPeekResult, _ := p.buf.Peek(bufferSize)
peekBuffer := bytes.NewBuffer(parserBufferPeekResult)
for {
peekData, peekErr := peekBuffer.ReadBytes('\n')
if peekErr != nil && peekErr != io.EOF {
p.debug("readPythonMultilines: failed to peek with error: %v", peekErr)
return "", peekErr
}
p.debug("readPythonMultilines: parsing %q", string(peekData))
peekMatches := pythonMultiline.FindStringSubmatch(string(peekData))
p.debug("readPythonMultilines: matched %d parts", len(peekMatches))
for n, v := range peekMatches {
p.debug(" %d: %q", n, v)
}
// Return if not a Python multiline value.
if len(peekMatches) != 3 {
p.debug("readPythonMultilines: end of value, got: %q", line)
return line, nil
}
// Advance the parser reader (buffer) in-sync with the peek buffer.
_, err := p.buf.Discard(len(peekData))
if err != nil {
p.debug("readPythonMultilines: failed to skip to the end, returning error")
return "", err
}
line += "\n" + peekMatches[0]
}
}
// parse parses data through an io.Reader.
func (f *File) parse(reader io.Reader) (err error) {
p := newParser(reader, parserOptions{
IgnoreContinuation: f.options.IgnoreContinuation,
IgnoreInlineComment: f.options.IgnoreInlineComment,
AllowPythonMultilineValues: f.options.AllowPythonMultilineValues,
SpaceBeforeInlineComment: f.options.SpaceBeforeInlineComment,
UnescapeValueDoubleQuotes: f.options.UnescapeValueDoubleQuotes,
UnescapeValueCommentSymbols: f.options.UnescapeValueCommentSymbols,
PreserveSurroundedQuote: f.options.PreserveSurroundedQuote,
DebugFunc: f.options.DebugFunc,
ReaderBufferSize: f.options.ReaderBufferSize,
})
if err = p.BOM(); err != nil {
return fmt.Errorf("BOM: %v", err)
}
// Ignore error because default section name is never empty string.
name := DefaultSection
if f.options.Insensitive || f.options.InsensitiveSections {
name = strings.ToLower(DefaultSection)
}
section, _ := f.NewSection(name)
// This "last" is not strictly equivalent to "previous one" if current key is not the first nested key
var isLastValueEmpty bool
var lastRegularKey *Key
var line []byte
var inUnparseableSection bool
// NOTE: Iterate and increase `currentPeekSize` until
// the size of the parser buffer is found.
// TODO(unknwon): When Golang 1.10 is the lowest version supported, replace with `parserBufferSize := p.buf.Size()`.
parserBufferSize := 0
// NOTE: Peek 4kb at a time.
currentPeekSize := minReaderBufferSize
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 {
return err
}
if f.options.AllowNestedValues &&
isLastValueEmpty && len(line) > 0 {
if line[0] == ' ' || line[0] == '\t' {
err = lastRegularKey.addNestedValue(string(bytes.TrimSpace(line)))
if err != nil {
return err
}
continue
}
}
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
if len(line) == 0 {
continue
}
// Comments
if line[0] == '#' || line[0] == ';' {
// Note: we do not care ending line break,
// it is needed for adding second line,
// so just clean it once at the end when set to value.
p.comment.Write(line)
continue
}
// Section
if line[0] == '[' {
// Read to the next ']' (TODO: support quoted strings)
closeIdx := bytes.LastIndexByte(line, ']')
if closeIdx == -1 {
return fmt.Errorf("unclosed section: %s", line)
}
name := string(line[1:closeIdx])
section, err = f.NewSection(name)
if err != nil {
return err
}
comment, has := cleanComment(line[closeIdx+1:])
if has {
p.comment.Write(comment)
}
section.Comment = strings.TrimSpace(p.comment.String())
// Reset auto-counter and comments
p.comment.Reset()
p.count = 1
// Nested values can't span sections
isLastValueEmpty = false
inUnparseableSection = false
for i := range f.options.UnparseableSections {
if f.options.UnparseableSections[i] == name ||
((f.options.Insensitive || f.options.InsensitiveSections) && strings.EqualFold(f.options.UnparseableSections[i], name)) {
inUnparseableSection = true
continue
}
}
continue
}
if inUnparseableSection {
section.isRawSection = true
section.rawBody += string(line)
continue
}
kname, offset, err := readKeyName(f.options.KeyValueDelimiters, line)
if err != nil {
switch {
// Treat as boolean key when desired, and whole line is key name.
case IsErrDelimiterNotFound(err):
switch {
case f.options.AllowBooleanKeys:
kname, err := p.readValue(line, parserBufferSize)
if err != nil {
return err
}
key, err := section.NewBooleanKey(kname)
if err != nil {
return err
}
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
continue
case f.options.SkipUnrecognizableLines:
continue
}
case IsErrEmptyKeyName(err) && f.options.SkipUnrecognizableLines:
continue
}
return err
}
// Auto increment.
isAutoIncr := false
if kname == "-" {
isAutoIncr = true
kname = "#" + strconv.Itoa(p.count)
p.count++
}
value, err := p.readValue(line[offset:], parserBufferSize)
if err != nil {
return err
}
isLastValueEmpty = len(value) == 0
key, err := section.NewKey(kname, value)
if err != nil {
return err
}
key.isAutoIncrement = isAutoIncr
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
lastRegularKey = key
}
return nil
}
ini-1.67.0/parser_test.go 0000664 0000000 0000000 00000003621 14274172402 0015253 0 ustar 00root root 0000000 0000000 // Copyright 2016 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBOM(t *testing.T) {
t.Run("test handling BOM", func(t *testing.T) {
t.Run("UTF-8-BOM", func(t *testing.T) {
f, err := Load("testdata/UTF-8-BOM.ini")
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "example@email.com", f.Section("author").Key("E-MAIL").String())
})
t.Run("UTF-16-LE-BOM", func(t *testing.T) {
f, err := Load("testdata/UTF-16-LE-BOM.ini")
require.NoError(t, err)
require.NotNil(t, f)
})
t.Run("UTF-16-BE-BOM", func(t *testing.T) {
})
})
}
func TestBadLoad(t *testing.T) {
t.Run("load with bad data", func(t *testing.T) {
t.Run("bad section name", func(t *testing.T) {
_, err := Load([]byte("[]"))
require.Error(t, err)
_, err = Load([]byte("["))
require.Error(t, err)
})
t.Run("bad keys", func(t *testing.T) {
_, err := Load([]byte(`"""name`))
require.Error(t, err)
_, err = Load([]byte(`"""name"""`))
require.Error(t, err)
_, err = Load([]byte(`""=1`))
require.Error(t, err)
_, err = Load([]byte(`=`))
require.Error(t, err)
_, err = Load([]byte(`name`))
require.Error(t, err)
})
t.Run("bad values", func(t *testing.T) {
_, err := Load([]byte(`name="""Unknwon`))
require.Error(t, err)
})
})
}
ini-1.67.0/section.go 0000664 0000000 0000000 00000013653 14274172402 0014372 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"errors"
"fmt"
"strings"
)
// Section represents a config section.
type Section struct {
f *File
Comment string
name string
keys map[string]*Key
keyList []string
keysHash map[string]string
isRawSection bool
rawBody string
}
func newSection(f *File, name string) *Section {
return &Section{
f: f,
name: name,
keys: make(map[string]*Key),
keyList: make([]string, 0, 10),
keysHash: make(map[string]string),
}
}
// Name returns name of Section.
func (s *Section) Name() string {
return s.name
}
// Body returns rawBody of Section if the section was marked as unparseable.
// It still follows the other rules of the INI format surrounding leading/trailing whitespace.
func (s *Section) Body() string {
return strings.TrimSpace(s.rawBody)
}
// SetBody updates body content only if section is raw.
func (s *Section) SetBody(body string) {
if !s.isRawSection {
return
}
s.rawBody = body
}
// NewKey creates a new key to given section.
func (s *Section) NewKey(name, val string) (*Key, error) {
if len(name) == 0 {
return nil, errors.New("error creating new key: empty key name")
} else if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
name = strings.ToLower(name)
}
if s.f.BlockMode {
s.f.lock.Lock()
defer s.f.lock.Unlock()
}
if inSlice(name, s.keyList) {
if s.f.options.AllowShadows {
if err := s.keys[name].addShadow(val); err != nil {
return nil, err
}
} else {
s.keys[name].value = val
s.keysHash[name] = val
}
return s.keys[name], nil
}
s.keyList = append(s.keyList, name)
s.keys[name] = newKey(s, name, val)
s.keysHash[name] = val
return s.keys[name], nil
}
// NewBooleanKey creates a new boolean type key to given section.
func (s *Section) NewBooleanKey(name string) (*Key, error) {
key, err := s.NewKey(name, "true")
if err != nil {
return nil, err
}
key.isBooleanType = true
return key, nil
}
// GetKey returns key in section by given name.
func (s *Section) GetKey(name string) (*Key, error) {
if s.f.BlockMode {
s.f.lock.RLock()
}
if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
name = strings.ToLower(name)
}
key := s.keys[name]
if s.f.BlockMode {
s.f.lock.RUnlock()
}
if key == nil {
// Check if it is a child-section.
sname := s.name
for {
if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
sname = sname[:i]
sec, err := s.f.GetSection(sname)
if err != nil {
continue
}
return sec.GetKey(name)
}
break
}
return nil, fmt.Errorf("error when getting key of section %q: key %q not exists", s.name, name)
}
return key, nil
}
// HasKey returns true if section contains a key with given name.
func (s *Section) HasKey(name string) bool {
key, _ := s.GetKey(name)
return key != nil
}
// Deprecated: Use "HasKey" instead.
func (s *Section) Haskey(name string) bool {
return s.HasKey(name)
}
// HasValue returns true if section contains given raw value.
func (s *Section) HasValue(value string) bool {
if s.f.BlockMode {
s.f.lock.RLock()
defer s.f.lock.RUnlock()
}
for _, k := range s.keys {
if value == k.value {
return true
}
}
return false
}
// Key assumes named Key exists in section and returns a zero-value when not.
func (s *Section) Key(name string) *Key {
key, err := s.GetKey(name)
if err != nil {
// It's OK here because the only possible error is empty key name,
// but if it's empty, this piece of code won't be executed.
key, _ = s.NewKey(name, "")
return key
}
return key
}
// Keys returns list of keys of section.
func (s *Section) Keys() []*Key {
keys := make([]*Key, len(s.keyList))
for i := range s.keyList {
keys[i] = s.Key(s.keyList[i])
}
return keys
}
// ParentKeys returns list of keys of parent section.
func (s *Section) ParentKeys() []*Key {
var parentKeys []*Key
sname := s.name
for {
if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
sname = sname[:i]
sec, err := s.f.GetSection(sname)
if err != nil {
continue
}
parentKeys = append(parentKeys, sec.Keys()...)
} else {
break
}
}
return parentKeys
}
// KeyStrings returns list of key names of section.
func (s *Section) KeyStrings() []string {
list := make([]string, len(s.keyList))
copy(list, s.keyList)
return list
}
// KeysHash returns keys hash consisting of names and values.
func (s *Section) KeysHash() map[string]string {
if s.f.BlockMode {
s.f.lock.RLock()
defer s.f.lock.RUnlock()
}
hash := make(map[string]string, len(s.keysHash))
for key, value := range s.keysHash {
hash[key] = value
}
return hash
}
// DeleteKey deletes a key from section.
func (s *Section) DeleteKey(name string) {
if s.f.BlockMode {
s.f.lock.Lock()
defer s.f.lock.Unlock()
}
for i, k := range s.keyList {
if k == name {
s.keyList = append(s.keyList[:i], s.keyList[i+1:]...)
delete(s.keys, name)
delete(s.keysHash, name)
return
}
}
}
// ChildSections returns a list of child sections of current section.
// For example, "[parent.child1]" and "[parent.child12]" are child sections
// of section "[parent]".
func (s *Section) ChildSections() []*Section {
prefix := s.name + s.f.options.ChildSectionDelimiter
children := make([]*Section, 0, 3)
for _, name := range s.f.sectionList {
if strings.HasPrefix(name, prefix) {
children = append(children, s.f.sections[name]...)
}
}
return children
}
ini-1.67.0/section_test.go 0000664 0000000 0000000 00000020137 14274172402 0015424 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSection_SetBody(t *testing.T) {
t.Run("set body of raw section", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
sec, err := f.NewRawSection("comments", `1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000`)
require.NoError(t, err)
require.NotNil(t, sec)
assert.Equal(t, `1111111111111111111000000000000000001110000
111111111111111111100000000000111000000000`, sec.Body())
sec.SetBody("1111111111111111111000000000000000001110000")
assert.Equal(t, `1111111111111111111000000000000000001110000`, sec.Body())
t.Run("set for non-raw section", func(t *testing.T) {
sec, err := f.NewSection("author")
require.NoError(t, err)
require.NotNil(t, sec)
assert.Empty(t, sec.Body())
sec.SetBody("1111111111111111111000000000000000001110000")
assert.Empty(t, sec.Body())
})
})
}
func TestSection_NewKey(t *testing.T) {
t.Run("create a new key", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, "NAME", k.Name())
assert.Equal(t, "ini", k.Value())
t.Run("with duplicated name", func(t *testing.T) {
k, err := f.Section("").NewKey("NAME", "ini.v1")
require.NoError(t, err)
require.NotNil(t, k)
// Overwrite previous existed key
assert.Equal(t, "ini.v1", k.Value())
})
t.Run("with empty string", func(t *testing.T) {
_, err := f.Section("").NewKey("", "")
require.Error(t, err)
})
})
t.Run("create keys with same name and allow shadow", func(t *testing.T) {
f, err := ShadowLoad([]byte(""))
require.NoError(t, err)
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("").NewKey("NAME", "ini.v1")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, []string{"ini", "ini.v1"}, k.ValueWithShadows())
})
}
func TestSection_NewBooleanKey(t *testing.T) {
t.Run("create a new boolean key", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewBooleanKey("start-ssh-server")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, "start-ssh-server", k.Name())
assert.Equal(t, "true", k.Value())
t.Run("with empty string", func(t *testing.T) {
_, err := f.Section("").NewBooleanKey("")
require.Error(t, err)
})
})
}
func TestSection_GetKey(t *testing.T) {
t.Run("get a key", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("").GetKey("NAME")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, "NAME", k.Name())
assert.Equal(t, "ini", k.Value())
t.Run("key not exists", func(t *testing.T) {
_, err := f.Section("").GetKey("404")
require.Error(t, err)
})
t.Run("key exists in parent section", func(t *testing.T) {
k, err := f.Section("parent").NewKey("AGE", "18")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("parent.child.son").GetKey("AGE")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, "18", k.Value())
})
})
}
func TestSection_HasKey(t *testing.T) {
t.Run("check if a key exists", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.True(t, f.Section("").HasKey("NAME"))
assert.True(t, f.Section("").HasKey("NAME"))
assert.False(t, f.Section("").HasKey("404"))
assert.False(t, f.Section("").HasKey("404"))
})
}
func TestSection_HasValue(t *testing.T) {
t.Run("check if contains a value in any key", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.True(t, f.Section("").HasValue("ini"))
assert.False(t, f.Section("").HasValue("404"))
})
}
func TestSection_Key(t *testing.T) {
t.Run("get a key", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
k = f.Section("").Key("NAME")
require.NotNil(t, k)
assert.Equal(t, "NAME", k.Name())
assert.Equal(t, "ini", k.Value())
t.Run("key not exists", func(t *testing.T) {
k := f.Section("").Key("404")
require.NotNil(t, k)
assert.Equal(t, "404", k.Name())
})
t.Run("key exists in parent section", func(t *testing.T) {
k, err := f.Section("parent").NewKey("AGE", "18")
require.NoError(t, err)
require.NotNil(t, k)
k = f.Section("parent.child.son").Key("AGE")
require.NotNil(t, k)
assert.Equal(t, "18", k.Value())
})
})
}
func TestSection_Keys(t *testing.T) {
t.Run("get all keys in a section", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("").NewKey("VERSION", "v1")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
require.NoError(t, err)
require.NotNil(t, k)
keys := f.Section("").Keys()
names := []string{"NAME", "VERSION", "IMPORT_PATH"}
assert.Equal(t, len(names), len(keys))
for i, name := range names {
assert.Equal(t, name, keys[i].Name())
}
})
}
func TestSection_ParentKeys(t *testing.T) {
t.Run("get all keys of parent sections", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("package").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("package").NewKey("VERSION", "v1")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("package").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
require.NoError(t, err)
require.NotNil(t, k)
keys := f.Section("package.sub.sub2").ParentKeys()
names := []string{"NAME", "VERSION", "IMPORT_PATH"}
assert.Equal(t, len(names), len(keys))
for i, name := range names {
assert.Equal(t, name, keys[i].Name())
}
})
}
func TestSection_KeyStrings(t *testing.T) {
t.Run("get all key names in a section", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("").NewKey("VERSION", "v1")
require.NoError(t, err)
require.NotNil(t, k)
k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
require.NoError(t, err)
require.NotNil(t, k)
assert.Equal(t, []string{"NAME", "VERSION", "IMPORT_PATH"}, f.Section("").KeyStrings())
})
}
func TestSection_KeyHash(t *testing.T) {
t.Run("get clone of key hash", func(t *testing.T) {
f, err := Load([]byte(`
key = one
[log]
name = app
file = a.log
`), []byte(`
key = two
[log]
name = app2
file = b.log
`))
require.NoError(t, err)
require.NotNil(t, f)
assert.Equal(t, "two", f.Section("").Key("key").String())
hash := f.Section("log").KeysHash()
relation := map[string]string{
"name": "app2",
"file": "b.log",
}
for k, v := range hash {
assert.Equal(t, relation[k], v)
}
})
}
func TestSection_DeleteKey(t *testing.T) {
t.Run("delete a key", func(t *testing.T) {
f := Empty()
require.NotNil(t, f)
k, err := f.Section("").NewKey("NAME", "ini")
require.NoError(t, err)
require.NotNil(t, k)
assert.True(t, f.Section("").HasKey("NAME"))
f.Section("").DeleteKey("NAME")
assert.False(t, f.Section("").HasKey("NAME"))
})
}
ini-1.67.0/struct.go 0000664 0000000 0000000 00000051317 14274172402 0014251 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"errors"
"fmt"
"reflect"
"strings"
"time"
"unicode"
)
// NameMapper represents a ini tag name mapper.
type NameMapper func(string) string
// Built-in name getters.
var (
// SnackCase converts to format SNACK_CASE.
SnackCase NameMapper = func(raw string) string {
newstr := make([]rune, 0, len(raw))
for i, chr := range raw {
if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
if i > 0 {
newstr = append(newstr, '_')
}
}
newstr = append(newstr, unicode.ToUpper(chr))
}
return string(newstr)
}
// TitleUnderscore converts to format title_underscore.
TitleUnderscore NameMapper = func(raw string) string {
newstr := make([]rune, 0, len(raw))
for i, chr := range raw {
if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
if i > 0 {
newstr = append(newstr, '_')
}
chr -= 'A' - 'a'
}
newstr = append(newstr, chr)
}
return string(newstr)
}
)
func (s *Section) parseFieldName(raw, actual string) string {
if len(actual) > 0 {
return actual
}
if s.f.NameMapper != nil {
return s.f.NameMapper(raw)
}
return raw
}
func parseDelim(actual string) string {
if len(actual) > 0 {
return actual
}
return ","
}
var reflectTime = reflect.TypeOf(time.Now()).Kind()
// setSliceWithProperType sets proper values to slice based on its type.
func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error {
var strs []string
if allowShadow {
strs = key.StringsWithShadows(delim)
} else {
strs = key.Strings(delim)
}
numVals := len(strs)
if numVals == 0 {
return nil
}
var vals interface{}
var err error
sliceOf := field.Type().Elem().Kind()
switch sliceOf {
case reflect.String:
vals = strs
case reflect.Int:
vals, err = key.parseInts(strs, true, false)
case reflect.Int64:
vals, err = key.parseInt64s(strs, true, false)
case reflect.Uint:
vals, err = key.parseUints(strs, true, false)
case reflect.Uint64:
vals, err = key.parseUint64s(strs, true, false)
case reflect.Float64:
vals, err = key.parseFloat64s(strs, true, false)
case reflect.Bool:
vals, err = key.parseBools(strs, true, false)
case reflectTime:
vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false)
default:
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
}
if err != nil && isStrict {
return err
}
slice := reflect.MakeSlice(field.Type(), numVals, numVals)
for i := 0; i < numVals; i++ {
switch sliceOf {
case reflect.String:
slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i]))
case reflect.Int:
slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i]))
case reflect.Int64:
slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i]))
case reflect.Uint:
slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i]))
case reflect.Uint64:
slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i]))
case reflect.Float64:
slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i]))
case reflect.Bool:
slice.Index(i).Set(reflect.ValueOf(vals.([]bool)[i]))
case reflectTime:
slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i]))
}
}
field.Set(slice)
return nil
}
func wrapStrictError(err error, isStrict bool) error {
if isStrict {
return err
}
return nil
}
// setWithProperType sets proper value to field based on its type,
// but it does not return error for failing parsing,
// because we want to use default value that is already assigned to struct.
func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error {
vt := t
isPtr := t.Kind() == reflect.Ptr
if isPtr {
vt = t.Elem()
}
switch vt.Kind() {
case reflect.String:
stringVal := key.String()
if isPtr {
field.Set(reflect.ValueOf(&stringVal))
} else if len(stringVal) > 0 {
field.SetString(key.String())
}
case reflect.Bool:
boolVal, err := key.Bool()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
field.Set(reflect.ValueOf(&boolVal))
} else {
field.SetBool(boolVal)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
// ParseDuration will not return err for `0`, so check the type name
if vt.Name() == "Duration" {
durationVal, err := key.Duration()
if err != nil {
if intVal, err := key.Int64(); err == nil {
field.SetInt(intVal)
return nil
}
return wrapStrictError(err, isStrict)
}
if isPtr {
field.Set(reflect.ValueOf(&durationVal))
} else if int64(durationVal) > 0 {
field.Set(reflect.ValueOf(durationVal))
}
return nil
}
intVal, err := key.Int64()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
pv := reflect.New(t.Elem())
pv.Elem().SetInt(intVal)
field.Set(pv)
} else {
field.SetInt(intVal)
}
// byte is an alias for uint8, so supporting uint8 breaks support for byte
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
durationVal, err := key.Duration()
// Skip zero value
if err == nil && uint64(durationVal) > 0 {
if isPtr {
field.Set(reflect.ValueOf(&durationVal))
} else {
field.Set(reflect.ValueOf(durationVal))
}
return nil
}
uintVal, err := key.Uint64()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
pv := reflect.New(t.Elem())
pv.Elem().SetUint(uintVal)
field.Set(pv)
} else {
field.SetUint(uintVal)
}
case reflect.Float32, reflect.Float64:
floatVal, err := key.Float64()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
pv := reflect.New(t.Elem())
pv.Elem().SetFloat(floatVal)
field.Set(pv)
} else {
field.SetFloat(floatVal)
}
case reflectTime:
timeVal, err := key.Time()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
field.Set(reflect.ValueOf(&timeVal))
} else {
field.Set(reflect.ValueOf(timeVal))
}
case reflect.Slice:
return setSliceWithProperType(key, field, delim, allowShadow, isStrict)
default:
return fmt.Errorf("unsupported type %q", t)
}
return nil
}
func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool, allowNonUnique bool, extends bool) {
opts := strings.SplitN(tag, ",", 5)
rawName = opts[0]
for _, opt := range opts[1:] {
omitEmpty = omitEmpty || (opt == "omitempty")
allowShadow = allowShadow || (opt == "allowshadow")
allowNonUnique = allowNonUnique || (opt == "nonunique")
extends = extends || (opt == "extends")
}
return rawName, omitEmpty, allowShadow, allowNonUnique, extends
}
// mapToField maps the given value to the matching field of the given section.
// The sectionIndex is the index (if non unique sections are enabled) to which the value should be added.
func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, sectionName string) error {
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
field := val.Field(i)
tpField := typ.Field(i)
tag := tpField.Tag.Get("ini")
if tag == "-" {
continue
}
rawName, _, allowShadow, allowNonUnique, extends := parseTagOptions(tag)
fieldName := s.parseFieldName(tpField.Name, rawName)
if len(fieldName) == 0 || !field.CanSet() {
continue
}
isStruct := tpField.Type.Kind() == reflect.Struct
isStructPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct
isAnonymousPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous
if isAnonymousPtr {
field.Set(reflect.New(tpField.Type.Elem()))
}
if extends && (isAnonymousPtr || (isStruct && tpField.Anonymous)) {
if isStructPtr && field.IsNil() {
field.Set(reflect.New(tpField.Type.Elem()))
}
fieldSection := s
if rawName != "" {
sectionName = s.name + s.f.options.ChildSectionDelimiter + rawName
if secs, err := s.f.SectionsByName(sectionName); err == nil && sectionIndex < len(secs) {
fieldSection = secs[sectionIndex]
}
}
if err := fieldSection.mapToField(field, isStrict, sectionIndex, sectionName); err != nil {
return fmt.Errorf("map to field %q: %v", fieldName, err)
}
} else if isAnonymousPtr || isStruct || isStructPtr {
if secs, err := s.f.SectionsByName(fieldName); err == nil {
if len(secs) <= sectionIndex {
return fmt.Errorf("there are not enough sections (%d <= %d) for the field %q", len(secs), sectionIndex, fieldName)
}
// Only set the field to non-nil struct value if we have a section for it.
// Otherwise, we end up with a non-nil struct ptr even though there is no data.
if isStructPtr && field.IsNil() {
field.Set(reflect.New(tpField.Type.Elem()))
}
if err = secs[sectionIndex].mapToField(field, isStrict, sectionIndex, fieldName); err != nil {
return fmt.Errorf("map to field %q: %v", fieldName, err)
}
continue
}
}
// Map non-unique sections
if allowNonUnique && tpField.Type.Kind() == reflect.Slice {
newField, err := s.mapToSlice(fieldName, field, isStrict)
if err != nil {
return fmt.Errorf("map to slice %q: %v", fieldName, err)
}
field.Set(newField)
continue
}
if key, err := s.GetKey(fieldName); err == nil {
delim := parseDelim(tpField.Tag.Get("delim"))
if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil {
return fmt.Errorf("set field %q: %v", fieldName, err)
}
}
}
return nil
}
// mapToSlice maps all sections with the same name and returns the new value.
// The type of the Value must be a slice.
func (s *Section) mapToSlice(secName string, val reflect.Value, isStrict bool) (reflect.Value, error) {
secs, err := s.f.SectionsByName(secName)
if err != nil {
return reflect.Value{}, err
}
typ := val.Type().Elem()
for i, sec := range secs {
elem := reflect.New(typ)
if err = sec.mapToField(elem, isStrict, i, sec.name); err != nil {
return reflect.Value{}, fmt.Errorf("map to field from section %q: %v", secName, err)
}
val = reflect.Append(val, elem.Elem())
}
return val, nil
}
// mapTo maps a section to object v.
func (s *Section) mapTo(v interface{}, isStrict bool) error {
typ := reflect.TypeOf(v)
val := reflect.ValueOf(v)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
} else {
return errors.New("not a pointer to a struct")
}
if typ.Kind() == reflect.Slice {
newField, err := s.mapToSlice(s.name, val, isStrict)
if err != nil {
return err
}
val.Set(newField)
return nil
}
return s.mapToField(val, isStrict, 0, s.name)
}
// MapTo maps section to given struct.
func (s *Section) MapTo(v interface{}) error {
return s.mapTo(v, false)
}
// StrictMapTo maps section to given struct in strict mode,
// which returns all possible error including value parsing error.
func (s *Section) StrictMapTo(v interface{}) error {
return s.mapTo(v, true)
}
// MapTo maps file to given struct.
func (f *File) MapTo(v interface{}) error {
return f.Section("").MapTo(v)
}
// StrictMapTo maps file to given struct in strict mode,
// which returns all possible error including value parsing error.
func (f *File) StrictMapTo(v interface{}) error {
return f.Section("").StrictMapTo(v)
}
// MapToWithMapper maps data sources to given struct with name mapper.
func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error {
cfg, err := Load(source, others...)
if err != nil {
return err
}
cfg.NameMapper = mapper
return cfg.MapTo(v)
}
// StrictMapToWithMapper maps data sources to given struct with name mapper in strict mode,
// which returns all possible error including value parsing error.
func StrictMapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error {
cfg, err := Load(source, others...)
if err != nil {
return err
}
cfg.NameMapper = mapper
return cfg.StrictMapTo(v)
}
// MapTo maps data sources to given struct.
func MapTo(v, source interface{}, others ...interface{}) error {
return MapToWithMapper(v, nil, source, others...)
}
// StrictMapTo maps data sources to given struct in strict mode,
// which returns all possible error including value parsing error.
func StrictMapTo(v, source interface{}, others ...interface{}) error {
return StrictMapToWithMapper(v, nil, source, others...)
}
// reflectSliceWithProperType does the opposite thing as setSliceWithProperType.
func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow bool) error {
slice := field.Slice(0, field.Len())
if field.Len() == 0 {
return nil
}
sliceOf := field.Type().Elem().Kind()
if allowShadow {
var keyWithShadows *Key
for i := 0; i < field.Len(); i++ {
var val string
switch sliceOf {
case reflect.String:
val = slice.Index(i).String()
case reflect.Int, reflect.Int64:
val = fmt.Sprint(slice.Index(i).Int())
case reflect.Uint, reflect.Uint64:
val = fmt.Sprint(slice.Index(i).Uint())
case reflect.Float64:
val = fmt.Sprint(slice.Index(i).Float())
case reflect.Bool:
val = fmt.Sprint(slice.Index(i).Bool())
case reflectTime:
val = slice.Index(i).Interface().(time.Time).Format(time.RFC3339)
default:
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
}
if i == 0 {
keyWithShadows = newKey(key.s, key.name, val)
} else {
_ = keyWithShadows.AddShadow(val)
}
}
*key = *keyWithShadows
return nil
}
var buf bytes.Buffer
for i := 0; i < field.Len(); i++ {
switch sliceOf {
case reflect.String:
buf.WriteString(slice.Index(i).String())
case reflect.Int, reflect.Int64:
buf.WriteString(fmt.Sprint(slice.Index(i).Int()))
case reflect.Uint, reflect.Uint64:
buf.WriteString(fmt.Sprint(slice.Index(i).Uint()))
case reflect.Float64:
buf.WriteString(fmt.Sprint(slice.Index(i).Float()))
case reflect.Bool:
buf.WriteString(fmt.Sprint(slice.Index(i).Bool()))
case reflectTime:
buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339))
default:
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
}
buf.WriteString(delim)
}
key.SetValue(buf.String()[:buf.Len()-len(delim)])
return nil
}
// reflectWithProperType does the opposite thing as setWithProperType.
func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow bool) error {
switch t.Kind() {
case reflect.String:
key.SetValue(field.String())
case reflect.Bool:
key.SetValue(fmt.Sprint(field.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
key.SetValue(fmt.Sprint(field.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
key.SetValue(fmt.Sprint(field.Uint()))
case reflect.Float32, reflect.Float64:
key.SetValue(fmt.Sprint(field.Float()))
case reflectTime:
key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339)))
case reflect.Slice:
return reflectSliceWithProperType(key, field, delim, allowShadow)
case reflect.Ptr:
if !field.IsNil() {
return reflectWithProperType(t.Elem(), key, field.Elem(), delim, allowShadow)
}
default:
return fmt.Errorf("unsupported type %q", t)
}
return nil
}
// CR: copied from encoding/json/encode.go with modifications of time.Time support.
// TODO: add more test coverage.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
case reflectTime:
t, ok := v.Interface().(time.Time)
return ok && t.IsZero()
}
return false
}
// StructReflector is the interface implemented by struct types that can extract themselves into INI objects.
type StructReflector interface {
ReflectINIStruct(*File) error
}
func (s *Section) reflectFrom(val reflect.Value) error {
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
if !val.Field(i).CanInterface() {
continue
}
field := val.Field(i)
tpField := typ.Field(i)
tag := tpField.Tag.Get("ini")
if tag == "-" {
continue
}
rawName, omitEmpty, allowShadow, allowNonUnique, extends := parseTagOptions(tag)
if omitEmpty && isEmptyValue(field) {
continue
}
if r, ok := field.Interface().(StructReflector); ok {
return r.ReflectINIStruct(s.f)
}
fieldName := s.parseFieldName(tpField.Name, rawName)
if len(fieldName) == 0 || !field.CanSet() {
continue
}
if extends && tpField.Anonymous && (tpField.Type.Kind() == reflect.Ptr || tpField.Type.Kind() == reflect.Struct) {
if err := s.reflectFrom(field); err != nil {
return fmt.Errorf("reflect from field %q: %v", fieldName, err)
}
continue
}
if (tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct) ||
(tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") {
// Note: The only error here is section doesn't exist.
sec, err := s.f.GetSection(fieldName)
if err != nil {
// Note: fieldName can never be empty here, ignore error.
sec, _ = s.f.NewSection(fieldName)
}
// Add comment from comment tag
if len(sec.Comment) == 0 {
sec.Comment = tpField.Tag.Get("comment")
}
if err = sec.reflectFrom(field); err != nil {
return fmt.Errorf("reflect from field %q: %v", fieldName, err)
}
continue
}
if allowNonUnique && tpField.Type.Kind() == reflect.Slice {
slice := field.Slice(0, field.Len())
if field.Len() == 0 {
return nil
}
sliceOf := field.Type().Elem().Kind()
for i := 0; i < field.Len(); i++ {
if sliceOf != reflect.Struct && sliceOf != reflect.Ptr {
return fmt.Errorf("field %q is not a slice of pointer or struct", fieldName)
}
sec, err := s.f.NewSection(fieldName)
if err != nil {
return err
}
// Add comment from comment tag
if len(sec.Comment) == 0 {
sec.Comment = tpField.Tag.Get("comment")
}
if err := sec.reflectFrom(slice.Index(i)); err != nil {
return fmt.Errorf("reflect from field %q: %v", fieldName, err)
}
}
continue
}
// Note: Same reason as section.
key, err := s.GetKey(fieldName)
if err != nil {
key, _ = s.NewKey(fieldName, "")
}
// Add comment from comment tag
if len(key.Comment) == 0 {
key.Comment = tpField.Tag.Get("comment")
}
delim := parseDelim(tpField.Tag.Get("delim"))
if err = reflectWithProperType(tpField.Type, key, field, delim, allowShadow); err != nil {
return fmt.Errorf("reflect field %q: %v", fieldName, err)
}
}
return nil
}
// ReflectFrom reflects section from given struct. It overwrites existing ones.
func (s *Section) ReflectFrom(v interface{}) error {
typ := reflect.TypeOf(v)
val := reflect.ValueOf(v)
if s.name != DefaultSection && s.f.options.AllowNonUniqueSections &&
(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Ptr) {
// Clear sections to make sure none exists before adding the new ones
s.f.DeleteSection(s.name)
if typ.Kind() == reflect.Ptr {
sec, err := s.f.NewSection(s.name)
if err != nil {
return err
}
return sec.reflectFrom(val.Elem())
}
slice := val.Slice(0, val.Len())
sliceOf := val.Type().Elem().Kind()
if sliceOf != reflect.Ptr {
return fmt.Errorf("not a slice of pointers")
}
for i := 0; i < slice.Len(); i++ {
sec, err := s.f.NewSection(s.name)
if err != nil {
return err
}
err = sec.reflectFrom(slice.Index(i))
if err != nil {
return fmt.Errorf("reflect from %dth field: %v", i, err)
}
}
return nil
}
if typ.Kind() == reflect.Ptr {
val = val.Elem()
} else {
return errors.New("not a pointer to a struct")
}
return s.reflectFrom(val)
}
// ReflectFrom reflects file from given struct.
func (f *File) ReflectFrom(v interface{}) error {
return f.Section("").ReflectFrom(v)
}
// ReflectFromWithMapper reflects data sources from given struct with name mapper.
func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error {
cfg.NameMapper = mapper
return cfg.ReflectFrom(v)
}
// ReflectFrom reflects data sources from given struct.
func ReflectFrom(cfg *File, v interface{}) error {
return ReflectFromWithMapper(cfg, v, nil)
}
ini-1.67.0/struct_test.go 0000664 0000000 0000000 00000055613 14274172402 0015313 0 ustar 00root root 0000000 0000000 // Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package ini
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testNested struct {
Cities []string `delim:"|"`
Visits []time.Time
Years []int
Numbers []int64
Ages []uint
Populations []uint64
Coordinates []float64
Flags []bool
Note string
Unused int `ini:"-"`
}
type TestEmbeded struct {
GPA float64
}
type testStruct struct {
Name string `ini:"NAME"`
Age int
Male bool
Money float64
Born time.Time
Time time.Duration `ini:"Duration"`
OldVersionTime time.Duration
Others testNested
OthersPtr *testNested
NilPtr *testNested
*TestEmbeded `ini:"grade"`
Unused int `ini:"-"`
Unsigned uint
Omitted bool `ini:"omitthis,omitempty"`
Shadows []string `ini:",allowshadow"`
ShadowInts []int `ini:"Shadows,allowshadow"`
BoolPtr *bool
BoolPtrNil *bool
FloatPtr *float64
FloatPtrNil *float64
IntPtr *int
IntPtrNil *int
UintPtr *uint
UintPtrNil *uint
StringPtr *string
StringPtrNil *string
TimePtr *time.Time
TimePtrNil *time.Time
DurationPtr *time.Duration
DurationPtrNil *time.Duration
}
type testInterface struct {
Address string
ListenPort int
PrivateKey string
}
type testPeer struct {
PublicKey string
PresharedKey string
AllowedIPs []string `delim:","`
}
type testNonUniqueSectionsStruct struct {
Interface testInterface
Peer []testPeer `ini:",nonunique"`
}
type BaseStruct struct {
Base bool
}
type testExtend struct {
BaseStruct `ini:",extends"`
Extend bool
}
const confDataStruct = `
NAME = Unknwon
Age = 21
Male = true
Money = 1.25
Born = 1993-10-07T20:17:05Z
Duration = 2h45m
OldVersionTime = 30
Unsigned = 3
omitthis = true
Shadows = 1, 2
Shadows = 3, 4
BoolPtr = false
FloatPtr = 0
IntPtr = 0
UintPtr = 0
StringPtr = ""
TimePtr = 0001-01-01T00:00:00Z
DurationPtr = 0s
[Others]
Cities = HangZhou|Boston
Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
Years = 1993,1994
Numbers = 10010,10086
Ages = 18,19
Populations = 12345678,98765432
Coordinates = 192.168,10.11
Flags = true,false
Note = Hello world!
[OthersPtr]
Cities = HangZhou|Boston
Visits = 1993-10-07T20:17:05Z, 1993-10-07T20:17:05Z
Years = 1993,1994
Numbers = 10010,10086
Ages = 18,19
Populations = 12345678,98765432
Coordinates = 192.168,10.11
Flags = true,false
Note = Hello world!
[grade]
GPA = 2.8
[foo.bar]
Here = there
When = then
[extended]
Base = true
Extend = true
`
const confNonUniqueSectionDataStruct = `[Interface]
Address = 10.2.0.1/24
ListenPort = 34777
PrivateKey = privServerKey
[Peer]
PublicKey = pubClientKey
PresharedKey = psKey
AllowedIPs = 10.2.0.2/32,fd00:2::2/128
[Peer]
PublicKey = pubClientKey2
PresharedKey = psKey2
AllowedIPs = 10.2.0.3/32,fd00:2::3/128
`
type unsupport struct {
Byte byte
}
type unsupport2 struct {
Others struct {
Cities byte
}
}
type Unsupport3 struct {
Cities byte
}
type unsupport4 struct {
*Unsupport3 `ini:"Others"`
}
type defaultValue struct {
Name string
Age int
Male bool
Optional *bool
Money float64
Born time.Time
Cities []string
}
type fooBar struct {
Here, When string
}
const invalidDataConfStruct = `
Name =
Age = age
Male = 123
Money = money
Born = nil
Cities =
`
func Test_MapToStruct(t *testing.T) {
t.Run("map to struct", func(t *testing.T) {
t.Run("map file to struct", func(t *testing.T) {
ts := new(testStruct)
assert.NoError(t, MapTo(ts, []byte(confDataStruct)))
assert.Equal(t, "Unknwon", ts.Name)
assert.Equal(t, 21, ts.Age)
assert.True(t, ts.Male)
assert.Equal(t, 1.25, ts.Money)
assert.Equal(t, uint(3), ts.Unsigned)
ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
require.NoError(t, err)
assert.Equal(t, ti.String(), ts.Born.String())
dur, err := time.ParseDuration("2h45m")
require.NoError(t, err)
assert.Equal(t, dur.Seconds(), ts.Time.Seconds())
assert.Equal(t, 30*time.Second, ts.OldVersionTime*time.Second)
assert.Equal(t, "HangZhou,Boston", strings.Join(ts.Others.Cities, ","))
assert.Equal(t, ti.String(), ts.Others.Visits[0].String())
assert.Equal(t, "[1993 1994]", fmt.Sprint(ts.Others.Years))
assert.Equal(t, "[10010 10086]", fmt.Sprint(ts.Others.Numbers))
assert.Equal(t, "[18 19]", fmt.Sprint(ts.Others.Ages))
assert.Equal(t, "[12345678 98765432]", fmt.Sprint(ts.Others.Populations))
assert.Equal(t, "[192.168 10.11]", fmt.Sprint(ts.Others.Coordinates))
assert.Equal(t, "[true false]", fmt.Sprint(ts.Others.Flags))
assert.Equal(t, "Hello world!", ts.Others.Note)
assert.Equal(t, 2.8, ts.TestEmbeded.GPA)
assert.Equal(t, "HangZhou,Boston", strings.Join(ts.OthersPtr.Cities, ","))
assert.Equal(t, ti.String(), ts.OthersPtr.Visits[0].String())
assert.Equal(t, "[1993 1994]", fmt.Sprint(ts.OthersPtr.Years))
assert.Equal(t, "[10010 10086]", fmt.Sprint(ts.OthersPtr.Numbers))
assert.Equal(t, "[18 19]", fmt.Sprint(ts.OthersPtr.Ages))
assert.Equal(t, "[12345678 98765432]", fmt.Sprint(ts.OthersPtr.Populations))
assert.Equal(t, "[192.168 10.11]", fmt.Sprint(ts.OthersPtr.Coordinates))
assert.Equal(t, "[true false]", fmt.Sprint(ts.OthersPtr.Flags))
assert.Equal(t, "Hello world!", ts.OthersPtr.Note)
assert.Nil(t, ts.NilPtr)
assert.Equal(t, false, *ts.BoolPtr)
assert.Nil(t, ts.BoolPtrNil)
assert.Equal(t, float64(0), *ts.FloatPtr)
assert.Nil(t, ts.FloatPtrNil)
assert.Equal(t, 0, *ts.IntPtr)
assert.Nil(t, ts.IntPtrNil)
assert.Equal(t, uint(0), *ts.UintPtr)
assert.Nil(t, ts.UintPtrNil)
assert.Equal(t, "", *ts.StringPtr)
assert.Nil(t, ts.StringPtrNil)
assert.NotNil(t, *ts.TimePtr)
assert.Nil(t, ts.TimePtrNil)
assert.Equal(t, time.Duration(0), *ts.DurationPtr)
assert.Nil(t, ts.DurationPtrNil)
})
t.Run("map section to struct", func(t *testing.T) {
foobar := new(fooBar)
f, err := Load([]byte(confDataStruct))
require.NoError(t, err)
assert.NoError(t, f.Section("foo.bar").MapTo(foobar))
assert.Equal(t, "there", foobar.Here)
assert.Equal(t, "then", foobar.When)
})
t.Run("map to non-pointer struct", func(t *testing.T) {
f, err := Load([]byte(confDataStruct))
require.NoError(t, err)
require.NotNil(t, f)
assert.Error(t, f.MapTo(testStruct{}))
})
t.Run("map to unsupported type", func(t *testing.T) {
f, err := Load([]byte(confDataStruct))
require.NoError(t, err)
require.NotNil(t, f)
f.NameMapper = func(raw string) string {
if raw == "Byte" {
return "NAME"
}
return raw
}
assert.Error(t, f.MapTo(&unsupport{}))
assert.Error(t, f.MapTo(&unsupport2{}))
assert.Error(t, f.MapTo(&unsupport4{}))
})
t.Run("map to omitempty field", func(t *testing.T) {
ts := new(testStruct)
assert.NoError(t, MapTo(ts, []byte(confDataStruct)))
assert.Equal(t, true, ts.Omitted)
})
t.Run("map with shadows", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowShadows: true}, []byte(confDataStruct))
require.NoError(t, err)
ts := new(testStruct)
assert.NoError(t, f.MapTo(ts))
assert.Equal(t, "1 2 3 4", strings.Join(ts.Shadows, " "))
assert.Equal(t, "[1 2 3 4]", fmt.Sprintf("%v", ts.ShadowInts))
})
t.Run("map from invalid data source", func(t *testing.T) {
assert.Error(t, MapTo(&testStruct{}, "hi"))
})
t.Run("map to wrong types and gain default values", func(t *testing.T) {
f, err := Load([]byte(invalidDataConfStruct))
require.NoError(t, err)
ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
require.NoError(t, err)
dv := &defaultValue{"Joe", 10, true, nil, 1.25, ti, []string{"HangZhou", "Boston"}}
assert.NoError(t, f.MapTo(dv))
assert.Equal(t, "Joe", dv.Name)
assert.Equal(t, 10, dv.Age)
assert.True(t, dv.Male)
assert.Equal(t, 1.25, dv.Money)
assert.Equal(t, ti.String(), dv.Born.String())
assert.Equal(t, "HangZhou,Boston", strings.Join(dv.Cities, ","))
})
t.Run("map to extended base", func(t *testing.T) {
f, err := Load([]byte(confDataStruct))
require.NoError(t, err)
require.NotNil(t, f)
te := testExtend{}
assert.NoError(t, f.Section("extended").MapTo(&te))
assert.True(t, te.Base)
assert.True(t, te.Extend)
})
})
t.Run("map to struct in strict mode", func(t *testing.T) {
f, err := Load([]byte(`
name=bruce
age=a30`))
require.NoError(t, err)
type Strict struct {
Name string `ini:"name"`
Age int `ini:"age"`
}
s := new(Strict)
assert.Error(t, f.Section("").StrictMapTo(s))
})
t.Run("map slice in strict mode", func(t *testing.T) {
f, err := Load([]byte(`
names=alice, bruce`))
require.NoError(t, err)
type Strict struct {
Names []string `ini:"names"`
}
s := new(Strict)
assert.NoError(t, f.Section("").StrictMapTo(s))
assert.Equal(t, "[alice bruce]", fmt.Sprint(s.Names))
})
}
func Test_MapToStructNonUniqueSections(t *testing.T) {
t.Run("map to struct non unique", func(t *testing.T) {
t.Run("map file to struct non unique", func(t *testing.T) {
f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
require.NoError(t, err)
ts := new(testNonUniqueSectionsStruct)
assert.NoError(t, f.MapTo(ts))
assert.Equal(t, "10.2.0.1/24", ts.Interface.Address)
assert.Equal(t, 34777, ts.Interface.ListenPort)
assert.Equal(t, "privServerKey", ts.Interface.PrivateKey)
assert.Equal(t, "pubClientKey", ts.Peer[0].PublicKey)
assert.Equal(t, "psKey", ts.Peer[0].PresharedKey)
assert.Equal(t, "10.2.0.2/32", ts.Peer[0].AllowedIPs[0])
assert.Equal(t, "fd00:2::2/128", ts.Peer[0].AllowedIPs[1])
assert.Equal(t, "pubClientKey2", ts.Peer[1].PublicKey)
assert.Equal(t, "psKey2", ts.Peer[1].PresharedKey)
assert.Equal(t, "10.2.0.3/32", ts.Peer[1].AllowedIPs[0])
assert.Equal(t, "fd00:2::3/128", ts.Peer[1].AllowedIPs[1])
})
t.Run("map non unique section to struct", func(t *testing.T) {
newPeer := new(testPeer)
newPeerSlice := make([]testPeer, 0)
f, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, []byte(confNonUniqueSectionDataStruct))
require.NoError(t, err)
// try only first one
assert.NoError(t, f.Section("Peer").MapTo(newPeer))
assert.Equal(t, "pubClientKey", newPeer.PublicKey)
assert.Equal(t, "psKey", newPeer.PresharedKey)
assert.Equal(t, "10.2.0.2/32", newPeer.AllowedIPs[0])
assert.Equal(t, "fd00:2::2/128", newPeer.AllowedIPs[1])
// try all
assert.NoError(t, f.Section("Peer").MapTo(&newPeerSlice))
assert.Equal(t, "pubClientKey", newPeerSlice[0].PublicKey)
assert.Equal(t, "psKey", newPeerSlice[0].PresharedKey)
assert.Equal(t, "10.2.0.2/32", newPeerSlice[0].AllowedIPs[0])
assert.Equal(t, "fd00:2::2/128", newPeerSlice[0].AllowedIPs[1])
assert.Equal(t, "pubClientKey2", newPeerSlice[1].PublicKey)
assert.Equal(t, "psKey2", newPeerSlice[1].PresharedKey)
assert.Equal(t, "10.2.0.3/32", newPeerSlice[1].AllowedIPs[0])
assert.Equal(t, "fd00:2::3/128", newPeerSlice[1].AllowedIPs[1])
})
t.Run("map non unique sections with subsections to struct", func(t *testing.T) {
iniFile, err := LoadSources(LoadOptions{AllowNonUniqueSections: true}, strings.NewReader(`
[Section]
FieldInSubSection = 1
FieldInSubSection2 = 2
FieldInSection = 3
[Section]
FieldInSubSection = 4
FieldInSubSection2 = 5
FieldInSection = 6
`))
require.NoError(t, err)
type SubSection struct {
FieldInSubSection string `ini:"FieldInSubSection"`
}
type SubSection2 struct {
FieldInSubSection2 string `ini:"FieldInSubSection2"`
}
type Section struct {
SubSection `ini:"Section"`
SubSection2 `ini:"Section"`
FieldInSection string `ini:"FieldInSection"`
}
type File struct {
Sections []Section `ini:"Section,nonunique"`
}
f := new(File)
err = iniFile.MapTo(f)
require.NoError(t, err)
assert.Equal(t, "1", f.Sections[0].FieldInSubSection)
assert.Equal(t, "2", f.Sections[0].FieldInSubSection2)
assert.Equal(t, "3", f.Sections[0].FieldInSection)
assert.Equal(t, "4", f.Sections[1].FieldInSubSection)
assert.Equal(t, "5", f.Sections[1].FieldInSubSection2)
assert.Equal(t, "6", f.Sections[1].FieldInSection)
})
})
}
func Test_ReflectFromStruct(t *testing.T) {
t.Run("reflect from struct", func(t *testing.T) {
type Embeded struct {
Dates []time.Time `delim:"|" comment:"Time data"`
Places []string
Years []int
Numbers []int64
Ages []uint
Populations []uint64
Coordinates []float64
Flags []bool
None []int
}
type Author struct {
Name string `ini:"NAME"`
Male bool
Optional *bool
Age int `comment:"Author's age"`
Height uint
GPA float64
Date time.Time
NeverMind string `ini:"-"`
ignored string
*Embeded `ini:"infos" comment:"Embeded section"`
}
ti, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
require.NoError(t, err)
a := &Author{"Unknwon", true, nil, 21, 100, 2.8, ti, "", "ignored",
&Embeded{
[]time.Time{ti, ti},
[]string{"HangZhou", "Boston"},
[]int{1993, 1994},
[]int64{10010, 10086},
[]uint{18, 19},
[]uint64{12345678, 98765432},
[]float64{192.168, 10.11},
[]bool{true, false},
[]int{},
}}
cfg := Empty()
assert.NoError(t, ReflectFrom(cfg, a))
var buf bytes.Buffer
_, err = cfg.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `NAME = Unknwon
Male = true
Optional =
; Author's age
Age = 21
Height = 100
GPA = 2.8
Date = 1993-10-07T20:17:05Z
; Embeded section
[infos]
; Time data
Dates = 1993-10-07T20:17:05Z|1993-10-07T20:17:05Z
Places = HangZhou,Boston
Years = 1993,1994
Numbers = 10010,10086
Ages = 18,19
Populations = 12345678,98765432
Coordinates = 192.168,10.11
Flags = true,false
None =
`,
buf.String(),
)
t.Run("reflect from non-point struct", func(t *testing.T) {
assert.Error(t, ReflectFrom(cfg, Author{}))
})
t.Run("reflect from struct with omitempty", func(t *testing.T) {
cfg := Empty()
type SpecialStruct struct {
FirstName string `ini:"first_name"`
LastName string `ini:"last_name,omitempty"`
JustOmitMe string `ini:"omitempty"`
LastLogin time.Time `ini:"last_login,omitempty"`
LastLogin2 time.Time `ini:",omitempty"`
NotEmpty int `ini:"omitempty"`
Number int64 `ini:",omitempty"`
Ages uint `ini:",omitempty"`
Population uint64 `ini:",omitempty"`
Coordinate float64 `ini:",omitempty"`
Flag bool `ini:",omitempty"`
Note *string `ini:",omitempty"`
}
special := &SpecialStruct{
FirstName: "John",
LastName: "Doe",
NotEmpty: 9,
}
assert.NoError(t, ReflectFrom(cfg, special))
var buf bytes.Buffer
_, err = cfg.WriteTo(&buf)
assert.Equal(t, `first_name = John
last_name = Doe
omitempty = 9
`,
buf.String(),
)
})
t.Run("reflect from struct with non-anonymous structure pointer", func(t *testing.T) {
cfg := Empty()
type Rpc struct {
Enable bool `ini:"enable"`
Type string `ini:"type"`
Address string `ini:"addr"`
Name string `ini:"name"`
}
type Cfg struct {
Rpc *Rpc `ini:"rpc"`
}
config := &Cfg{
Rpc: &Rpc{
Enable: true,
Type: "type",
Address: "address",
Name: "name",
},
}
assert.NoError(t, cfg.ReflectFrom(config))
var buf bytes.Buffer
_, err = cfg.WriteTo(&buf)
assert.Equal(t, `[rpc]
enable = true
type = type
addr = address
name = name
`,
buf.String(),
)
})
})
}
func Test_ReflectFromStructNonUniqueSections(t *testing.T) {
t.Run("reflect from struct with non unique sections", func(t *testing.T) {
nonUnique := &testNonUniqueSectionsStruct{
Interface: testInterface{
Address: "10.2.0.1/24",
ListenPort: 34777,
PrivateKey: "privServerKey",
},
Peer: []testPeer{
{
PublicKey: "pubClientKey",
PresharedKey: "psKey",
AllowedIPs: []string{"10.2.0.2/32,fd00:2::2/128"},
},
{
PublicKey: "pubClientKey2",
PresharedKey: "psKey2",
AllowedIPs: []string{"10.2.0.3/32,fd00:2::3/128"},
},
},
}
cfg := Empty(LoadOptions{
AllowNonUniqueSections: true,
})
assert.NoError(t, ReflectFrom(cfg, nonUnique))
var buf bytes.Buffer
_, err := cfg.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, confNonUniqueSectionDataStruct, buf.String())
// note: using ReflectFrom from should overwrite the existing sections
err = cfg.Section("Peer").ReflectFrom([]*testPeer{
{
PublicKey: "pubClientKey3",
PresharedKey: "psKey3",
AllowedIPs: []string{"10.2.0.4/32,fd00:2::4/128"},
},
{
PublicKey: "pubClientKey4",
PresharedKey: "psKey4",
AllowedIPs: []string{"10.2.0.5/32,fd00:2::5/128"},
},
})
require.NoError(t, err)
buf = bytes.Buffer{}
_, err = cfg.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[Interface]
Address = 10.2.0.1/24
ListenPort = 34777
PrivateKey = privServerKey
[Peer]
PublicKey = pubClientKey3
PresharedKey = psKey3
AllowedIPs = 10.2.0.4/32,fd00:2::4/128
[Peer]
PublicKey = pubClientKey4
PresharedKey = psKey4
AllowedIPs = 10.2.0.5/32,fd00:2::5/128
`,
buf.String(),
)
// note: using ReflectFrom from should overwrite the existing sections
err = cfg.Section("Peer").ReflectFrom(&testPeer{
PublicKey: "pubClientKey5",
PresharedKey: "psKey5",
AllowedIPs: []string{"10.2.0.6/32,fd00:2::6/128"},
})
require.NoError(t, err)
buf = bytes.Buffer{}
_, err = cfg.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[Interface]
Address = 10.2.0.1/24
ListenPort = 34777
PrivateKey = privServerKey
[Peer]
PublicKey = pubClientKey5
PresharedKey = psKey5
AllowedIPs = 10.2.0.6/32,fd00:2::6/128
`,
buf.String(),
)
})
}
// Inspired by https://github.com/go-ini/ini/issues/196
func TestMapToAndReflectFromStructWithShadows(t *testing.T) {
t.Run("map to struct and then reflect with shadows should generate original config content", func(t *testing.T) {
type include struct {
Paths []string `ini:"path,omitempty,allowshadow"`
}
cfg, err := LoadSources(LoadOptions{
AllowShadows: true,
}, []byte(`
[include]
path = /tmp/gpm-profiles/test5.profile
path = /tmp/gpm-profiles/test1.profile`))
require.NoError(t, err)
sec := cfg.Section("include")
inc := new(include)
err = sec.MapTo(inc)
require.NoError(t, err)
err = sec.ReflectFrom(inc)
require.NoError(t, err)
var buf bytes.Buffer
_, err = cfg.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `[include]
path = /tmp/gpm-profiles/test5.profile
path = /tmp/gpm-profiles/test1.profile
`,
buf.String(),
)
t.Run("reflect from struct with shadows", func(t *testing.T) {
cfg := Empty(LoadOptions{
AllowShadows: true,
})
type ShadowStruct struct {
StringArray []string `ini:"sa,allowshadow"`
EmptyStringArrat []string `ini:"empty,omitempty,allowshadow"`
Allowshadow []string `ini:"allowshadow,allowshadow"`
Dates []time.Time `ini:",allowshadow"`
Places []string `ini:",allowshadow"`
Years []int `ini:",allowshadow"`
Numbers []int64 `ini:",allowshadow"`
Ages []uint `ini:",allowshadow"`
Populations []uint64 `ini:",allowshadow"`
Coordinates []float64 `ini:",allowshadow"`
Flags []bool `ini:",allowshadow"`
None []int `ini:",allowshadow"`
}
shadow := &ShadowStruct{
StringArray: []string{"s1", "s2"},
Allowshadow: []string{"s3", "s4"},
Dates: []time.Time{time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC),
time.Date(2020, 9, 12, 00, 00, 00, 651387237, time.UTC)},
Places: []string{"HangZhou", "Boston"},
Years: []int{1993, 1994},
Numbers: []int64{10010, 10086},
Ages: []uint{18, 19},
Populations: []uint64{12345678, 98765432},
Coordinates: []float64{192.168, 10.11},
Flags: []bool{true, false},
None: []int{},
}
assert.NoError(t, ReflectFrom(cfg, shadow))
var buf bytes.Buffer
_, err := cfg.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `sa = s1
sa = s2
allowshadow = s3
allowshadow = s4
Dates = 2020-09-12T00:00:00Z
Places = HangZhou
Places = Boston
Years = 1993
Years = 1994
Numbers = 10010
Numbers = 10086
Ages = 18
Ages = 19
Populations = 12345678
Populations = 98765432
Coordinates = 192.168
Coordinates = 10.11
Flags = true
Flags = false
None =
`,
buf.String(),
)
})
})
}
type testMapper struct {
PackageName string
}
func Test_NameGetter(t *testing.T) {
t.Run("test name mappers", func(t *testing.T) {
assert.NoError(t, MapToWithMapper(&testMapper{}, TitleUnderscore, []byte("packag_name=ini")))
cfg, err := Load([]byte("PACKAGE_NAME=ini"))
require.NoError(t, err)
require.NotNil(t, cfg)
cfg.NameMapper = SnackCase
tg := new(testMapper)
assert.NoError(t, cfg.MapTo(tg))
assert.Equal(t, "ini", tg.PackageName)
})
}
type testDurationStruct struct {
Duration time.Duration `ini:"Duration"`
}
func Test_Duration(t *testing.T) {
t.Run("duration less than 16m50s", func(t *testing.T) {
ds := new(testDurationStruct)
assert.NoError(t, MapTo(ds, []byte("Duration=16m49s")))
dur, err := time.ParseDuration("16m49s")
require.NoError(t, err)
assert.Equal(t, dur.Seconds(), ds.Duration.Seconds())
})
}
type Employer struct {
Name string
Title string
}
type Employers []*Employer
func (es Employers) ReflectINIStruct(f *File) error {
for _, e := range es {
f.Section(e.Name).Key("Title").SetValue(e.Title)
}
return nil
}
// Inspired by https://github.com/go-ini/ini/issues/199
func Test_StructReflector(t *testing.T) {
t.Run("reflect with StructReflector interface", func(t *testing.T) {
p := &struct {
FirstName string
Employer Employers
}{
FirstName: "Andrew",
Employer: []*Employer{
{
Name: `Employer "VMware"`,
Title: "Staff II Engineer",
},
{
Name: `Employer "EMC"`,
Title: "Consultant Engineer",
},
},
}
f := Empty()
assert.NoError(t, f.ReflectFrom(p))
var buf bytes.Buffer
_, err := f.WriteTo(&buf)
require.NoError(t, err)
assert.Equal(t, `FirstName = Andrew
[Employer "VMware"]
Title = Staff II Engineer
[Employer "EMC"]
Title = Consultant Engineer
`,
buf.String(),
)
})
}
ini-1.67.0/testdata/ 0000775 0000000 0000000 00000000000 14274172402 0014200 5 ustar 00root root 0000000 0000000 ini-1.67.0/testdata/TestFile_WriteTo.golden 0000664 0000000 0000000 00000003643 14274172402 0020574 0 ustar 00root root 0000000 0000000 ; Package name
NAME = ini
; Package version
VERSION = v1
; Package import path
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
; Information about package author
# Bio can be written in multiple lines.
[author]
; This is author name
NAME = Unknwon
E-MAIL = u@gogs.io
GITHUB = https://github.com/%(NAME)s
# Succeeding comment
BIO = """Gopher.
Coding addict.
Good man.
"""
[package]
CLONE_URL = https://%(IMPORT_PATH)s
[package.sub]
UNUSED_KEY = should be deleted
[features]
- = Support read/write comments of keys and sections
- = Support auto-increment of key names
- = Support load multiple files to overwrite key values
[types]
STRING = str
BOOL = true
BOOL_FALSE = false
FLOAT64 = 1.25
INT = 10
TIME = 2015-01-01T20:17:05Z
DURATION = 2h45m
UINT = 3
HEX_NUMBER = 0x3000
[array]
STRINGS = en, zh, de
FLOAT64S = 1.1, 2.2, 3.3
INTS = 1, 2, 3
UINTS = 1, 2, 3
TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z
BOOLS = true, false, false
[note]
empty_lines = next line is empty
boolean_key
more = notes
; Comment before the section
; This is a comment for the section too
[comments]
; Comment before key
key = value
; This is a comment for key2
key2 = value2
key3 = "one", "two", "three"
[string escapes]
key1 = value1, value2, value3
key2 = value1\, value2
key3 = val\ue1, value2
key4 = value1\\, value\\\\2
key5 = value1\,, value2
key6 = aaa bbb\ and\ space ccc
[advance]
value with quotes = some value
value quote2 again = some value
includes comment sign = `my#password`
includes comment sign2 = `my;password`
true = 2+3=5
`1+1=2` = true
`6+1=7` = true
"""`5+5`""" = 10
`"6+6"` = 12
`7-2=4` = false
ADDRESS = """404 road,
NotFound, State, 50000"""
two_lines = how about continuation lines?
lots_of_lines = "1 2 3 4 "
ini-1.67.0/testdata/UTF-16-BE-BOM.ini 0000664 0000000 0000000 00000000110 14274172402 0016452 0 ustar 00root root 0000000 0000000 þÿ [ a u t h o r ]
E - M A I L = e x a m p l e @ e m a i l . c o m ini-1.67.0/testdata/UTF-16-LE-BOM.ini 0000664 0000000 0000000 00000000110 14274172402 0016464 0 ustar 00root root 0000000 0000000 ÿþ[ a u t h o r ]
E - M A I L = e x a m p l e @ e m a i l . c o m ini-1.67.0/testdata/UTF-8-BOM.ini 0000664 0000000 0000000 00000000046 14274172402 0016117 0 ustar 00root root 0000000 0000000 [author]
E-MAIL = example@email.com ini-1.67.0/testdata/full.ini 0000664 0000000 0000000 00000003542 14274172402 0015647 0 ustar 00root root 0000000 0000000 ; Package name
NAME = ini
; Package version
VERSION = v1
; Package import path
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
# Information about package author
# Bio can be written in multiple lines.
[author]
NAME = Unknwon
E-MAIL = u@gogs.io
GITHUB = https://github.com/%(NAME)s
BIO = """Gopher.
Coding addict.
Good man.
""" # Succeeding comment
[package]
CLONE_URL = https://%(IMPORT_PATH)s
[package.sub]
UNUSED_KEY = should be deleted
[features]
-: Support read/write comments of keys and sections
-: Support auto-increment of key names
-: Support load multiple files to overwrite key values
[types]
STRING = str
BOOL = true
BOOL_FALSE = false
FLOAT64 = 1.25
INT = 10
TIME = 2015-01-01T20:17:05Z
DURATION = 2h45m
UINT = 3
HEX_NUMBER = 0x3000
[array]
STRINGS = en, zh, de
FLOAT64S = 1.1, 2.2, 3.3
INTS = 1, 2, 3
UINTS = 1, 2, 3
TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z
BOOLS = true, false, false
[note]
empty_lines = next line is empty\
; Comment before the section
[comments] ; This is a comment for the section too
; Comment before key
key = "value"
key2 = "value2" ; This is a comment for key2
key3 = "one", "two", "three"
[string escapes]
key1 = value1, value2, value3
key2 = value1\, value2
key3 = val\ue1, value2
key4 = value1\\, value\\\\2
key5 = value1\,, value2
key6 = aaa bbb\ and\ space ccc
[advance]
value with quotes = "some value"
value quote2 again = 'some value'
includes comment sign = `my#password`
includes comment sign2 = `my;password`
true = 2+3=5
"1+1=2" = true
"""6+1=7""" = true
"""`5+5`""" = 10
`"6+6"` = 12
`7-2=4` = false
ADDRESS = `404 road,
NotFound, State, 50000`
two_lines = how about \
continuation lines?
lots_of_lines = 1 \
2 \
3 \
4 \
ini-1.67.0/testdata/minimal.ini 0000664 0000000 0000000 00000000033 14274172402 0016323 0 ustar 00root root 0000000 0000000 [author]
E-MAIL = u@gogs.io ini-1.67.0/testdata/multiline.ini 0000664 0000000 0000000 00000021365 14274172402 0016712 0 ustar 00root root 0000000 0000000 value1 = some text here
some more text here
there is an empty line above and below
value2 = there is an empty line above
that is not indented so it should not be part
of the value
value3 = .
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu consequat ac felis donec et odio pellentesque diam volutpat. Mauris commodo quis imperdiet massa tincidunt nunc. Interdum velit euismod in pellentesque. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Nascetur ridiculus mus mauris vitae. Posuere urna nec tincidunt praesent semper feugiat. Lorem donec massa sapien faucibus et molestie ac feugiat sed. Ipsum dolor sit amet consectetur adipiscing elit. Enim sed faucibus turpis in eu mi. A diam sollicitudin tempor id. Quam nulla porttitor massa id neque aliquam vestibulum morbi blandit.
Lectus sit amet est placerat in egestas. At risus viverra adipiscing at in tellus integer. Tristique senectus et netus et malesuada fames ac. In hac habitasse platea dictumst. Purus in mollis nunc sed. Pellentesque sit amet porttitor eget dolor morbi. Elit at imperdiet dui accumsan sit amet nulla. Cursus in hac habitasse platea dictumst. Bibendum arcu vitae elementum curabitur. Faucibus ornare suspendisse sed nisi lacus. In vitae turpis massa sed. Libero nunc consequat interdum varius sit amet. Molestie a iaculis at erat pellentesque.
Dui faucibus in ornare quam viverra orci sagittis eu. Purus in mollis nunc sed id semper. Sed arcu non odio euismod lacinia at. Quis commodo odio aenean sed adipiscing diam donec. Quisque id diam vel quam elementum pulvinar. Lorem ipsum dolor sit amet. Purus ut faucibus pulvinar elementum integer enim neque volutpat ac. Fermentum posuere urna nec tincidunt praesent semper feugiat nibh sed. Gravida rutrum quisque non tellus orci. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant. Et sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Eget gravida cum sociis natoque penatibus et magnis. Elementum eu facilisis sed odio morbi quis commodo. Mollis nunc sed id semper risus in hendrerit gravida rutrum. Lorem dolor sed viverra ipsum.
Pellentesque adipiscing commodo elit at imperdiet dui accumsan sit amet. Justo eget magna fermentum iaculis eu non diam. Condimentum mattis pellentesque id nibh tortor id aliquet lectus. Tellus molestie nunc non blandit massa enim. Mauris ultrices eros in cursus turpis. Purus viverra accumsan in nisl nisi scelerisque. Quis lectus nulla at volutpat. Purus ut faucibus pulvinar elementum integer enim. In pellentesque massa placerat duis ultricies lacus sed turpis. Elit sed vulputate mi sit amet mauris commodo. Tellus elementum sagittis vitae et. Duis tristique sollicitudin nibh sit amet commodo nulla facilisi nullam. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Libero id faucibus nisl tincidunt eget nullam. Mattis aliquam faucibus purus in massa tempor. Fames ac turpis egestas sed tempus urna. Gravida in fermentum et sollicitudin ac orci phasellus egestas.
Blandit turpis cursus in hac habitasse. Sed id semper risus in. Amet porttitor eget dolor morbi non arcu. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt. Ut morbi tincidunt augue interdum velit. Lorem mollis aliquam ut porttitor leo a. Nunc eget lorem dolor sed viverra. Scelerisque mauris pellentesque pulvinar pellentesque. Elit at imperdiet dui accumsan sit amet. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Laoreet non curabitur gravida arcu ac tortor dignissim. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae purus. Lacus sed viverra tellus in hac habitasse platea dictumst vestibulum. Viverra adipiscing at in tellus. Duis at tellus at urna condimentum. Eget gravida cum sociis natoque penatibus et magnis dis parturient. Pharetra massa massa ultricies mi quis hendrerit.
Mauris pellentesque pulvinar pellentesque habitant morbi tristique. Maecenas volutpat blandit aliquam etiam. Sed turpis tincidunt id aliquet. Eget duis at tellus at urna condimentum. Pellentesque habitant morbi tristique senectus et. Amet aliquam id diam maecenas. Volutpat est velit egestas dui id. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel orci. Massa sed elementum tempus egestas sed sed risus pretium. Quam quisque id diam vel quam elementum pulvinar etiam non. Sapien faucibus et molestie ac. Ipsum dolor sit amet consectetur adipiscing. Viverra orci sagittis eu volutpat. Leo urna molestie at elementum. Commodo viverra maecenas accumsan lacus. Non sodales neque sodales ut etiam sit amet. Habitant morbi tristique senectus et netus et malesuada fames. Habitant morbi tristique senectus et netus et malesuada. Blandit aliquam etiam erat velit scelerisque in. Varius duis at consectetur lorem donec massa sapien faucibus et.
Augue mauris augue neque gravida in. Odio ut sem nulla pharetra diam sit amet nisl suscipit. Nulla aliquet enim tortor at auctor urna nunc id. Morbi tristique senectus et netus et malesuada fames ac. Quam id leo in vitae turpis massa sed elementum tempus. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Maecenas volutpat blandit aliquam etiam erat velit scelerisque in. Sagittis nisl rhoncus mattis rhoncus urna neque viverra justo. Massa tempor nec feugiat nisl pretium. Vulputate sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum. Enim lobortis scelerisque fermentum dui faucibus in ornare. Faucibus ornare suspendisse sed nisi lacus. Morbi tristique senectus et netus et malesuada fames. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Dictum non consectetur a erat nam at. Leo urna molestie at elementum eu facilisis sed odio morbi. Quam id leo in vitae turpis massa. Neque egestas congue quisque egestas diam in arcu. Varius morbi enim nunc faucibus a pellentesque sit. Aliquet enim tortor at auctor urna.
Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Luctus accumsan tortor posuere ac. Eu ultrices vitae auctor eu augue ut lectus arcu bibendum. Pretium nibh ipsum consequat nisl vel pretium lectus. Aliquam etiam erat velit scelerisque in dictum. Sem et tortor consequat id porta nibh venenatis cras sed. A scelerisque purus semper eget duis at tellus at urna. At auctor urna nunc id. Ornare quam viverra orci sagittis eu volutpat odio. Nisl purus in mollis nunc sed id semper. Ornare suspendisse sed nisi lacus sed. Consectetur lorem donec massa sapien faucibus et. Ipsum dolor sit amet consectetur adipiscing elit ut. Porta nibh venenatis cras sed. Dignissim diam quis enim lobortis scelerisque. Quam nulla porttitor massa id. Tellus molestie nunc non blandit massa.
Malesuada fames ac turpis egestas. Suscipit tellus mauris a diam maecenas. Turpis in eu mi bibendum neque egestas. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet. Quis imperdiet massa tincidunt nunc pulvinar sapien et. Urna duis convallis convallis tellus id. Velit egestas dui id ornare arcu odio. Consectetur purus ut faucibus pulvinar elementum integer enim neque. Aenean sed adipiscing diam donec adipiscing tristique. Tortor aliquam nulla facilisi cras fermentum odio eu. Diam in arcu cursus euismod quis viverra nibh cras.
Id ornare arcu odio ut sem. Arcu dictum varius duis at consectetur lorem donec massa sapien. Proin libero nunc consequat interdum varius sit. Ut eu sem integer vitae justo. Vitae elementum curabitur vitae nunc. Diam quam nulla porttitor massa. Lectus mauris ultrices eros in cursus turpis massa tincidunt dui. Natoque penatibus et magnis dis parturient montes. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Libero nunc consequat interdum varius sit. Rhoncus dolor purus non enim praesent. Pellentesque sit amet porttitor eget. Nibh tortor id aliquet lectus proin nibh. Fermentum iaculis eu non diam phasellus vestibulum lorem sed.
Eu feugiat pretium nibh ipsum consequat nisl vel pretium lectus. Habitant morbi tristique senectus et netus et malesuada fames ac. Urna condimentum mattis pellentesque id. Lorem sed risus ultricies tristique nulla aliquet enim tortor at. Ipsum dolor sit amet consectetur adipiscing elit. Convallis a cras semper auctor neque vitae tempus quam. A diam sollicitudin tempor id eu nisl nunc mi ipsum. Maecenas sed enim ut sem viverra aliquet eget. Massa enim nec dui nunc mattis enim. Nam aliquam sem et tortor consequat. Adipiscing commodo elit at imperdiet dui accumsan sit amet nulla. Nullam eget felis eget nunc lobortis. Mauris a diam maecenas sed enim ut sem viverra. Ornare massa eget egestas purus. In hac habitasse platea dictumst. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Nisl nunc mi ipsum faucibus. At varius vel pharetra vel. Mauris ultrices eros in cursus turpis massa tincidunt.
ini-1.67.0/testdata/multiline_eof.ini 0000664 0000000 0000000 00000000056 14274172402 0017535 0 ustar 00root root 0000000 0000000 value1 = some text here
some more text here 2