Compare commits
94 commits
eacb903dfd
...
master
Author | SHA1 | Date | |
---|---|---|---|
88fec2a7e4 | |||
b73d97f24a | |||
f97abaf30d | |||
801d808ab4 | |||
f136235205 | |||
1e4f7189a5 | |||
65506c733f | |||
3c83e46a30 | |||
1cde32efd1 | |||
32fe1612f5 | |||
ca55bff79d | |||
bd29f67142 | |||
0638e00677 | |||
9715790574 | |||
6167914277 | |||
05eba761ee | |||
ce2529fa9d | |||
a345a8d93e | |||
7c29e44f20 | |||
1354d931b2 | |||
6ce2c1d89d | |||
ebfd15b6d3 | |||
7cbdd46fde | |||
b78ef63bdf | |||
23ae291361 | |||
cbb970c293 | |||
559afb01fa | |||
60fe0deb91 | |||
925a3b4afa | |||
0905642feb | |||
8778d42e78 | |||
068deccf96 | |||
5574ec3267 | |||
e9598831cc | |||
cd2f58c920 | |||
c975a2ec14 | |||
c9c8e70afb | |||
493729cc3c | |||
c01b61cb11 | |||
5720ccd003 | |||
185e1129bc | |||
42a20c3dad | |||
0ce311a1eb | |||
3ff68bfaf8 | |||
bfd4172e48 | |||
65744b0e13 | |||
37cead20f3 | |||
267c53f15d | |||
60f95a0358 | |||
99f0400118 | |||
300fb6bbfb | |||
48d41b81e9 | |||
bac41a802a | |||
16496d91a1 | |||
3e97f07b28 | |||
a50544b43a | |||
781040a88b | |||
83c4757e36 | |||
2fadd81287 | |||
17ba292934 | |||
6b8386d449 | |||
531b28ed2b | |||
ba312b9244 | |||
df69cf8dba | |||
6b9fb8f8e5 | |||
23e2a37785 | |||
a10567e770 | |||
42a48efe9d | |||
19e18df973 | |||
38dc0f0b8f | |||
0bd39a308c | |||
f99da79ad9 | |||
df6937ade1 | |||
19cb3117eb | |||
48d7e1b65b | |||
3094dfd2c0 | |||
75a99679a2 | |||
3f8fdd74dc | |||
a53a056cea | |||
6c2385d394 | |||
ea0f76d528 | |||
f82d32953e | |||
f10afb7894 | |||
7f7a7e073d | |||
6a99b1caa0 | |||
eaa0a1766b | |||
fcda1d4666 | |||
3bd46913d3 | |||
594ee8ac0c | |||
2fb1fa3ff4 | |||
93e6e2eed3 | |||
1c770a9560 | |||
19fc0ebaaf | |||
c244975a0a |
23 changed files with 5929 additions and 1649 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
tarpaulin-report.html
|
13
.vscode/settings.json
vendored
Normal file
13
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"Multivar",
|
||||||
|
"autocrlf",
|
||||||
|
"bstr",
|
||||||
|
"combinator",
|
||||||
|
"gitea",
|
||||||
|
"gpgsign",
|
||||||
|
"implicits",
|
||||||
|
"multivars",
|
||||||
|
"subname"
|
||||||
|
]
|
||||||
|
}
|
27
Cargo.toml
27
Cargo.toml
|
@ -1,14 +1,31 @@
|
||||||
[package]
|
[package]
|
||||||
name = "serde-git-config"
|
name = "git-config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
repository = "https://github.com/Byron/gitoxide"
|
||||||
|
description = "A git-config file parser and editor from the gitoxide project"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
authors = ["Edward Shen <code@eddie.sh>"]
|
authors = ["Edward Shen <code@eddie.sh>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
keywords = ["git-config", "git", "config", "gitoxide"]
|
||||||
|
categories = ["config", "parser-implementations"]
|
||||||
|
exclude = ["fuzz/**/*", ".vscode/**/*", "benches/**/*"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
[features]
|
||||||
|
# serde = ["serde_crate"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0"
|
memchr = "2"
|
||||||
nom = "6"
|
nom = { version = "6", default_features = false, features = ["std"] }
|
||||||
|
serde_crate = { version = "1", package = "serde", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
criterion = "0.3"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "large_config_file"
|
||||||
|
harness = false
|
||||||
|
|
1
LICENSE-APACHE
Symbolic link
1
LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../LICENSE-APACHE
|
1
LICENSE-MIT
Symbolic link
1
LICENSE-MIT
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../LICENSE-MIT
|
75
README.md
Normal file
75
README.md
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# git-config
|
||||||
|
|
||||||
|
**git-config is a library for interacting with `git-config` files.**
|
||||||
|
|
||||||
|
This crate intents to be a performant Rust implementation for reading and
|
||||||
|
writing `git-config` files. It exposes tiers of abstractions, from simple
|
||||||
|
config value wrappers to a high level reader and writer.
|
||||||
|
|
||||||
|
The highlight of this crate is the zero-copy parser. We employ techniques to
|
||||||
|
avoid copying where necessary, and reads that do not need normalization are
|
||||||
|
guaranteed to be zero-copy. Higher level abstractions maintain this guarantee,
|
||||||
|
and utilizes acceleration structures for increased performance.
|
||||||
|
|
||||||
|
Currently, this is _not_ a binary. While we do intent to have a drop-in
|
||||||
|
replacement for the `git config` sub-command, we're currently missing
|
||||||
|
system-level abstractions to do so.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Reading and writing to a config:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use git_config::file::GitConfig;
|
||||||
|
use git_config::values::Boolean;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
|
let input = r#"
|
||||||
|
[core]
|
||||||
|
some-bool = true
|
||||||
|
|
||||||
|
[other "internal"]
|
||||||
|
hello = world
|
||||||
|
"#;
|
||||||
|
let mut config = GitConfig::from(input)?;
|
||||||
|
let boolean = config.get_config::<Boolean>("core", None, "some-bool");
|
||||||
|
config.set_value("other", Some("internal"), "hello", "clippy!");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are always welcome!
|
||||||
|
|
||||||
|
### Code quality
|
||||||
|
|
||||||
|
This repository enables pedantic, cargo, and nursery `clippy` lints. Make sure
|
||||||
|
to run `cargo clean && cargo clippy` (the clean stage is very important!) to
|
||||||
|
ensure your code is linted.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Since this is a performance oriented crate, in addition to well tested code via
|
||||||
|
`cargo test`, we also perform benchmarks to measure notable gains or losses in
|
||||||
|
performance. We use [`criterion`] so benches can be run via `cargo bench` after
|
||||||
|
installing it via `cargo install cargo-criterion`.
|
||||||
|
|
||||||
|
Changes to `parser.rs` may include a request to fuzz to ensure that it cannot
|
||||||
|
panic on inputs. This can be done by executing `cargo fuzz parser` after
|
||||||
|
installing the `fuzz` sub-command via `cargo install cargo-fuzz`.
|
||||||
|
|
||||||
|
#### License
|
||||||
|
|
||||||
|
<sup>
|
||||||
|
Licensed under either of <a href="LICENSE-APACHE">Apache License, Version
|
||||||
|
2.0</a> or <a href="LICENSE-MIT">MIT license</a> at your option.
|
||||||
|
</sup>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<sub>
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
|
for inclusion in git-config by you, as defined in the Apache-2.0 license, shall
|
||||||
|
be dual licensed as above, without any additional terms or conditions.
|
||||||
|
</sub>
|
||||||
|
|
||||||
|
[`criterion`]: https://github.com/bheisler/criterion.rs
|
293
benches/large_config_file.rs
Normal file
293
benches/large_config_file.rs
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use git_config::{file::GitConfig, parser::Parser};
|
||||||
|
|
||||||
|
fn git_config(c: &mut Criterion) {
|
||||||
|
c.bench_function("GitConfig large config file", |b| {
|
||||||
|
b.iter(|| GitConfig::try_from(black_box(CONFIG_FILE)).unwrap())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parser(c: &mut Criterion) {
|
||||||
|
c.bench_function("Parser large config file", |b| {
|
||||||
|
b.iter(|| Parser::try_from(black_box(CONFIG_FILE)).unwrap())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, git_config, parser);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
// Found from https://gist.github.com/pksunkara/988716
|
||||||
|
const CONFIG_FILE: &str = r#"[user]
|
||||||
|
name = Pavan Kumar Sunkara
|
||||||
|
email = pavan.sss1991@gmail.com
|
||||||
|
username = pksunkara
|
||||||
|
[core]
|
||||||
|
editor = vim
|
||||||
|
whitespace = fix,-indent-with-non-tab,trailing-space,cr-at-eol
|
||||||
|
pager = delta
|
||||||
|
[sendemail]
|
||||||
|
smtpencryption = tls
|
||||||
|
smtpserver = smtp.gmail.com
|
||||||
|
smtpuser = pavan.sss1991@gmail.com
|
||||||
|
smtppass = password
|
||||||
|
smtpserverport = 587
|
||||||
|
[web]
|
||||||
|
browser = google-chrome
|
||||||
|
[instaweb]
|
||||||
|
httpd = apache2 -f
|
||||||
|
[rerere]
|
||||||
|
enabled = 1
|
||||||
|
autoupdate = 1
|
||||||
|
[push]
|
||||||
|
default = matching
|
||||||
|
[color]
|
||||||
|
ui = auto
|
||||||
|
[color "branch"]
|
||||||
|
current = yellow bold
|
||||||
|
local = green bold
|
||||||
|
remote = cyan bold
|
||||||
|
[color "diff"]
|
||||||
|
meta = yellow bold
|
||||||
|
frag = magenta bold
|
||||||
|
old = red bold
|
||||||
|
new = green bold
|
||||||
|
whitespace = red reverse
|
||||||
|
[color "status"]
|
||||||
|
added = green bold
|
||||||
|
changed = yellow bold
|
||||||
|
untracked = red bold
|
||||||
|
[diff]
|
||||||
|
tool = vimdiff
|
||||||
|
[difftool]
|
||||||
|
prompt = false
|
||||||
|
[delta]
|
||||||
|
features = line-numbers decorations
|
||||||
|
line-numbers = true
|
||||||
|
[delta "decorations"]
|
||||||
|
minus-style = red bold normal
|
||||||
|
plus-style = green bold normal
|
||||||
|
minus-emph-style = white bold red
|
||||||
|
minus-non-emph-style = red bold normal
|
||||||
|
plus-emph-style = white bold green
|
||||||
|
plus-non-emph-style = green bold normal
|
||||||
|
file-style = yellow bold none
|
||||||
|
file-decoration-style = yellow box
|
||||||
|
hunk-header-style = magenta bold
|
||||||
|
hunk-header-decoration-style = magenta box
|
||||||
|
minus-empty-line-marker-style = normal normal
|
||||||
|
plus-empty-line-marker-style = normal normal
|
||||||
|
line-numbers-right-format = "{np:^4}│ "
|
||||||
|
[github]
|
||||||
|
user = pksunkara
|
||||||
|
token = token
|
||||||
|
[gitflow "prefix"]
|
||||||
|
versiontag = v
|
||||||
|
[sequence]
|
||||||
|
editor = interactive-rebase-tool
|
||||||
|
[alias]
|
||||||
|
a = add --all
|
||||||
|
ai = add -i
|
||||||
|
#############
|
||||||
|
ap = apply
|
||||||
|
as = apply --stat
|
||||||
|
ac = apply --check
|
||||||
|
#############
|
||||||
|
ama = am --abort
|
||||||
|
amr = am --resolved
|
||||||
|
ams = am --skip
|
||||||
|
#############
|
||||||
|
b = branch
|
||||||
|
ba = branch -a
|
||||||
|
bd = branch -d
|
||||||
|
bdd = branch -D
|
||||||
|
br = branch -r
|
||||||
|
bc = rev-parse --abbrev-ref HEAD
|
||||||
|
bu = !git rev-parse --abbrev-ref --symbolic-full-name "@{u}"
|
||||||
|
bs = !git-branch-status
|
||||||
|
#############
|
||||||
|
c = commit
|
||||||
|
ca = commit -a
|
||||||
|
cm = commit -m
|
||||||
|
cam = commit -am
|
||||||
|
cem = commit --allow-empty -m
|
||||||
|
cd = commit --amend
|
||||||
|
cad = commit -a --amend
|
||||||
|
ced = commit --allow-empty --amend
|
||||||
|
#############
|
||||||
|
cl = clone
|
||||||
|
cld = clone --depth 1
|
||||||
|
clg = !sh -c 'git clone git://github.com/$1 $(basename $1)' -
|
||||||
|
clgp = !sh -c 'git clone git@github.com:$1 $(basename $1)' -
|
||||||
|
clgu = !sh -c 'git clone git@github.com:$(git config --get user.username)/$1 $1' -
|
||||||
|
#############
|
||||||
|
cp = cherry-pick
|
||||||
|
cpa = cherry-pick --abort
|
||||||
|
cpc = cherry-pick --continue
|
||||||
|
#############
|
||||||
|
d = diff
|
||||||
|
dp = diff --patience
|
||||||
|
dc = diff --cached
|
||||||
|
dk = diff --check
|
||||||
|
dck = diff --cached --check
|
||||||
|
dt = difftool
|
||||||
|
dct = difftool --cached
|
||||||
|
#############
|
||||||
|
f = fetch
|
||||||
|
fo = fetch origin
|
||||||
|
fu = fetch upstream
|
||||||
|
#############
|
||||||
|
fp = format-patch
|
||||||
|
#############
|
||||||
|
fk = fsck
|
||||||
|
#############
|
||||||
|
g = grep -p
|
||||||
|
#############
|
||||||
|
l = log --oneline
|
||||||
|
lg = log --oneline --graph --decorate
|
||||||
|
#############
|
||||||
|
ls = ls-files
|
||||||
|
lsf = !git ls-files | grep -i
|
||||||
|
#############
|
||||||
|
m = merge
|
||||||
|
ma = merge --abort
|
||||||
|
mc = merge --continue
|
||||||
|
ms = merge --skip
|
||||||
|
#############
|
||||||
|
o = checkout
|
||||||
|
om = checkout master
|
||||||
|
ob = checkout -b
|
||||||
|
opr = !sh -c 'git fo pull/$1/head:pr-$1 && git o pr-$1'
|
||||||
|
#############
|
||||||
|
pr = prune -v
|
||||||
|
#############
|
||||||
|
ps = push
|
||||||
|
psf = push -f
|
||||||
|
psu = push -u
|
||||||
|
pst = push --tags
|
||||||
|
#############
|
||||||
|
pso = push origin
|
||||||
|
psao = push --all origin
|
||||||
|
psfo = push -f origin
|
||||||
|
psuo = push -u origin
|
||||||
|
#############
|
||||||
|
psom = push origin master
|
||||||
|
psaom = push --all origin master
|
||||||
|
psfom = push -f origin master
|
||||||
|
psuom = push -u origin master
|
||||||
|
psoc = !git push origin $(git bc)
|
||||||
|
psaoc = !git push --all origin $(git bc)
|
||||||
|
psfoc = !git push -f origin $(git bc)
|
||||||
|
psuoc = !git push -u origin $(git bc)
|
||||||
|
psdc = !git push origin :$(git bc)
|
||||||
|
#############
|
||||||
|
pl = pull
|
||||||
|
pb = pull --rebase
|
||||||
|
#############
|
||||||
|
plo = pull origin
|
||||||
|
pbo = pull --rebase origin
|
||||||
|
plom = pull origin master
|
||||||
|
ploc = !git pull origin $(git bc)
|
||||||
|
pbom = pull --rebase origin master
|
||||||
|
pboc = !git pull --rebase origin $(git bc)
|
||||||
|
#############
|
||||||
|
plu = pull upstream
|
||||||
|
plum = pull upstream master
|
||||||
|
pluc = !git pull upstream $(git bc)
|
||||||
|
pbum = pull --rebase upstream master
|
||||||
|
pbuc = !git pull --rebase upstream $(git bc)
|
||||||
|
#############
|
||||||
|
rb = rebase
|
||||||
|
rba = rebase --abort
|
||||||
|
rbc = rebase --continue
|
||||||
|
rbi = rebase --interactive
|
||||||
|
rbs = rebase --skip
|
||||||
|
#############
|
||||||
|
re = reset
|
||||||
|
rh = reset HEAD
|
||||||
|
reh = reset --hard
|
||||||
|
rem = reset --mixed
|
||||||
|
res = reset --soft
|
||||||
|
rehh = reset --hard HEAD
|
||||||
|
remh = reset --mixed HEAD
|
||||||
|
resh = reset --soft HEAD
|
||||||
|
rehom = reset --hard origin/master
|
||||||
|
#############
|
||||||
|
r = remote
|
||||||
|
ra = remote add
|
||||||
|
rr = remote rm
|
||||||
|
rv = remote -v
|
||||||
|
rn = remote rename
|
||||||
|
rp = remote prune
|
||||||
|
rs = remote show
|
||||||
|
rao = remote add origin
|
||||||
|
rau = remote add upstream
|
||||||
|
rro = remote remove origin
|
||||||
|
rru = remote remove upstream
|
||||||
|
rso = remote show origin
|
||||||
|
rsu = remote show upstream
|
||||||
|
rpo = remote prune origin
|
||||||
|
rpu = remote prune upstream
|
||||||
|
#############
|
||||||
|
rmf = rm -f
|
||||||
|
rmrf = rm -r -f
|
||||||
|
#############
|
||||||
|
s = status
|
||||||
|
sb = status -s -b
|
||||||
|
#############
|
||||||
|
sa = stash apply
|
||||||
|
sc = stash clear
|
||||||
|
sd = stash drop
|
||||||
|
sl = stash list
|
||||||
|
sp = stash pop
|
||||||
|
ss = stash save
|
||||||
|
ssk = stash save -k
|
||||||
|
sw = stash show
|
||||||
|
st = !git stash list | wc -l 2>/dev/null | grep -oEi '[0-9][0-9]*'
|
||||||
|
#############
|
||||||
|
t = tag
|
||||||
|
td = tag -d
|
||||||
|
#############
|
||||||
|
w = show
|
||||||
|
wp = show -p
|
||||||
|
wr = show -p --no-color
|
||||||
|
#############
|
||||||
|
svnr = svn rebase
|
||||||
|
svnd = svn dcommit
|
||||||
|
svnl = svn log --oneline --show-commit
|
||||||
|
#############
|
||||||
|
subadd = !sh -c 'git submodule add git://github.com/$1 $2/$(basename $1)' -
|
||||||
|
subrm = !sh -c 'git submodule deinit -f -- $1 && rm -rf .git/modules/$1 && git rm -f $1' -
|
||||||
|
subup = submodule update --init --recursive
|
||||||
|
subpull = !git submodule foreach git pull --tags origin master
|
||||||
|
#############
|
||||||
|
assume = update-index --assume-unchanged
|
||||||
|
unassume = update-index --no-assume-unchanged
|
||||||
|
assumed = !git ls -v | grep ^h | cut -c 3-
|
||||||
|
unassumeall = !git assumed | xargs git unassume
|
||||||
|
assumeall = !git status -s | awk {'print $2'} | xargs git assume
|
||||||
|
#############
|
||||||
|
bump = !sh -c 'git commit -am \"Version bump v$1\" && git psuoc && git release $1' -
|
||||||
|
release = !sh -c 'git tag v$1 && git pst' -
|
||||||
|
unrelease = !sh -c 'git tag -d v$1 && git pso :v$1' -
|
||||||
|
merged = !sh -c 'git o master && git plom && git bd $1 && git rpo' -
|
||||||
|
aliases = !git config -l | grep alias | cut -c 7-
|
||||||
|
snap = !git stash save 'snapshot: $(date)' && git stash apply 'stash@{0}'
|
||||||
|
bare = !sh -c 'git symbolic-ref HEAD refs/heads/$1 && git rm --cached -r . && git clean -xfd' -
|
||||||
|
whois = !sh -c 'git log -i -1 --author=\"$1\" --pretty=\"format:%an <%ae>\"' -
|
||||||
|
serve = daemon --reuseaddr --verbose --base-path=. --export-all ./.git
|
||||||
|
#############
|
||||||
|
behind = !git rev-list --left-only --count $(git bu)...HEAD
|
||||||
|
ahead = !git rev-list --right-only --count $(git bu)...HEAD
|
||||||
|
#############
|
||||||
|
ours = "!f() { git checkout --ours $@ && git add $@; }; f"
|
||||||
|
theirs = "!f() { git checkout --theirs $@ && git add $@; }; f"
|
||||||
|
subrepo = !sh -c 'git filter-branch --prune-empty --subdirectory-filter $1 master' -
|
||||||
|
human = name-rev --name-only --refs=refs/heads/*
|
||||||
|
[filter "lfs"]
|
||||||
|
clean = git-lfs clean -- %f
|
||||||
|
smudge = git-lfs smudge -- %f
|
||||||
|
process = git-lfs filter-process
|
||||||
|
required = true
|
||||||
|
"#;
|
4
fuzz/.gitignore
vendored
Normal file
4
fuzz/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
target
|
||||||
|
corpus
|
||||||
|
artifacts
|
26
fuzz/Cargo.toml
Normal file
26
fuzz/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "git-config-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
authors = ["Automatically generated"]
|
||||||
|
publish = false
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
cargo-fuzz = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libfuzzer-sys = "0.3"
|
||||||
|
|
||||||
|
[dependencies.git-config]
|
||||||
|
path = ".."
|
||||||
|
|
||||||
|
# Prevent this from interfering with workspaces
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "parser"
|
||||||
|
path = "fuzz_targets/parser.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
9
fuzz/fuzz_targets/parser.rs
Normal file
9
fuzz/fuzz_targets/parser.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use git_config::parser::Parser;
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
// Don't name this _; Rust may optimize it out.
|
||||||
|
let _a = Parser::from_bytes(data);
|
||||||
|
});
|
221
src/config.rs
221
src/config.rs
|
@ -1,221 +0,0 @@
|
||||||
use crate::parser::{parse_from_str, Event, ParsedSectionHeader, Parser};
|
|
||||||
use crate::values::Value;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{collections::HashMap, convert::TryFrom, io::Read};
|
|
||||||
|
|
||||||
type SectionConfig<'a> = HashMap<&'a str, Value<'a>>;
|
|
||||||
|
|
||||||
/// This struct provides a high level wrapper to access `git-config` file. This
|
|
||||||
/// struct exists primarily for reading a config rather than modifying it, as
|
|
||||||
/// it discards comments and unnecessary whitespace.
|
|
||||||
#[derive(Clone, Eq, PartialEq, Debug, Default)]
|
|
||||||
pub struct GitConfig<'a>(HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>);
|
|
||||||
|
|
||||||
const EMPTY_MARKER: &str = "@"; // Guaranteed to not be a {sub,}section or name.
|
|
||||||
|
|
||||||
impl<'a> GitConfig<'a> {
|
|
||||||
/// Attempts to construct a instance given a [`Parser`] instance.
|
|
||||||
///
|
|
||||||
/// This is _not_ a zero-copy operation. Due to how partial values may be
|
|
||||||
/// provided, we necessarily need to copy and store these values until we
|
|
||||||
/// are done.
|
|
||||||
pub fn try_from_parser_with_options(
|
|
||||||
parser: Parser<'a>,
|
|
||||||
options: ConfigOptions,
|
|
||||||
) -> Result<Self, ()> {
|
|
||||||
Self::try_from_event_iter_with_options(parser.into_iter(), options)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_from_event_iter_with_options(
|
|
||||||
iter: impl Iterator<Item = Event<'a>>,
|
|
||||||
options: ConfigOptions,
|
|
||||||
) -> Result<Self, ()> {
|
|
||||||
let mut sections: HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>> = HashMap::new();
|
|
||||||
let mut current_section_name = EMPTY_MARKER;
|
|
||||||
let mut current_subsection_name = EMPTY_MARKER;
|
|
||||||
let mut ignore_until_next_section = false;
|
|
||||||
let mut current_key = EMPTY_MARKER;
|
|
||||||
let mut value_scratch = String::new();
|
|
||||||
|
|
||||||
for event in iter {
|
|
||||||
match event {
|
|
||||||
Event::Comment(_) => (),
|
|
||||||
Event::SectionHeader(ParsedSectionHeader {
|
|
||||||
name,
|
|
||||||
subsection_name,
|
|
||||||
}) => {
|
|
||||||
current_section_name = name;
|
|
||||||
match (sections.get_mut(name), options.on_duplicate_section) {
|
|
||||||
(Some(_), OnDuplicateBehavior::Error) => todo!(),
|
|
||||||
(Some(section), OnDuplicateBehavior::Overwrite) => {
|
|
||||||
section.clear();
|
|
||||||
}
|
|
||||||
(Some(_), OnDuplicateBehavior::KeepExisting) => {
|
|
||||||
ignore_until_next_section = true;
|
|
||||||
}
|
|
||||||
(None, _) => {
|
|
||||||
sections.insert(name, HashMap::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match subsection_name {
|
|
||||||
Some(v) => current_subsection_name = v,
|
|
||||||
None => {
|
|
||||||
current_subsection_name = EMPTY_MARKER;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// subsection parsing
|
|
||||||
|
|
||||||
match (
|
|
||||||
sections
|
|
||||||
.get_mut(current_section_name)
|
|
||||||
.unwrap() // Guaranteed to exist at this point
|
|
||||||
.get_mut(current_subsection_name),
|
|
||||||
options.on_duplicate_section,
|
|
||||||
) {
|
|
||||||
(Some(_), OnDuplicateBehavior::Error) => todo!(),
|
|
||||||
(Some(section), OnDuplicateBehavior::Overwrite) => section.clear(),
|
|
||||||
(Some(_), OnDuplicateBehavior::KeepExisting) => {
|
|
||||||
ignore_until_next_section = true;
|
|
||||||
}
|
|
||||||
(None, _) => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ if ignore_until_next_section => (),
|
|
||||||
Event::Key(key) => {
|
|
||||||
current_key = key;
|
|
||||||
}
|
|
||||||
Event::Value(v) => {
|
|
||||||
Self::insert_value(
|
|
||||||
&mut sections,
|
|
||||||
current_section_name,
|
|
||||||
current_subsection_name,
|
|
||||||
current_key,
|
|
||||||
v,
|
|
||||||
options.on_duplicate_name,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Event::Newline(_) => (),
|
|
||||||
Event::ValueNotDone(v) => value_scratch.push_str(v),
|
|
||||||
Event::ValueDone(v) => {
|
|
||||||
let mut completed_value = String::new();
|
|
||||||
value_scratch.push_str(v);
|
|
||||||
std::mem::swap(&mut completed_value, &mut value_scratch);
|
|
||||||
Self::insert_value(
|
|
||||||
&mut sections,
|
|
||||||
current_section_name,
|
|
||||||
current_subsection_name,
|
|
||||||
current_key,
|
|
||||||
Value::from_string(completed_value),
|
|
||||||
options.on_duplicate_name,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self(sections))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_value(
|
|
||||||
map: &mut HashMap<&'a str, HashMap<&'a str, SectionConfig<'a>>>,
|
|
||||||
section: &str,
|
|
||||||
subsection: &str,
|
|
||||||
key: &'a str,
|
|
||||||
value: Value<'a>,
|
|
||||||
on_dup: OnDuplicateBehavior,
|
|
||||||
) -> Result<(), ()> {
|
|
||||||
let config = map.get_mut(section).unwrap().get_mut(subsection).unwrap();
|
|
||||||
|
|
||||||
if config.contains_key(key) {
|
|
||||||
match on_dup {
|
|
||||||
OnDuplicateBehavior::Error => return Err(()),
|
|
||||||
OnDuplicateBehavior::Overwrite => {
|
|
||||||
config.insert(key, value);
|
|
||||||
}
|
|
||||||
OnDuplicateBehavior::KeepExisting => (),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_section(&self, section_name: &str) -> Option<&SectionConfig<'_>> {
|
|
||||||
self.get_subsection(section_name, EMPTY_MARKER)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_section_value(&self, section_name: &str, key: &str) -> Option<&Value<'_>> {
|
|
||||||
self.get_section(section_name)
|
|
||||||
.map(|section| section.get(key))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_subsection(
|
|
||||||
&self,
|
|
||||||
section_name: &str,
|
|
||||||
subsection_name: &str,
|
|
||||||
) -> Option<&SectionConfig<'_>> {
|
|
||||||
self.0
|
|
||||||
.get(section_name)
|
|
||||||
.map(|subsections| subsections.get(subsection_name))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_subsection_value(
|
|
||||||
&self,
|
|
||||||
section_name: &str,
|
|
||||||
subsection_name: &str,
|
|
||||||
key: &str,
|
|
||||||
) -> Option<&Value<'_>> {
|
|
||||||
self.get_subsection(section_name, subsection_name)
|
|
||||||
.map(|section| section.get(key))
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TryFrom<Parser<'a>> for GitConfig<'a> {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(parser: Parser<'a>) -> Result<Self, Self::Error> {
|
|
||||||
Self::try_from_parser_with_options(parser, ConfigOptions::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
|
||||||
pub struct ConfigOptions {
|
|
||||||
on_duplicate_section: OnDuplicateBehavior,
|
|
||||||
on_duplicate_name: OnDuplicateBehavior,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigOptions {
|
|
||||||
pub fn on_duplicate_section(&mut self, behavior: OnDuplicateBehavior) -> &mut Self {
|
|
||||||
self.on_duplicate_section = behavior;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_duplicate_name(&mut self, behavior: OnDuplicateBehavior) -> &mut Self {
|
|
||||||
self.on_duplicate_name = behavior;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [`GitConfig`]'s valid possible actions when encountering a duplicate section
|
|
||||||
/// or key name within a section.
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
|
||||||
pub enum OnDuplicateBehavior {
|
|
||||||
/// Fail the operation, returning an error instead. This is the strictest
|
|
||||||
/// behavior, and is the default.
|
|
||||||
Error,
|
|
||||||
/// Discard any data we had before on the
|
|
||||||
Overwrite,
|
|
||||||
KeepExisting,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for OnDuplicateBehavior {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Error
|
|
||||||
}
|
|
||||||
}
|
|
410
src/de.rs
410
src/de.rs
|
@ -1,410 +0,0 @@
|
||||||
use std::ops::{AddAssign, MulAssign, Neg};
|
|
||||||
|
|
||||||
use crate::values::Boolean;
|
|
||||||
use crate::{
|
|
||||||
error::{Error, Result},
|
|
||||||
values::PeekParse,
|
|
||||||
};
|
|
||||||
use serde::de::{
|
|
||||||
self, DeserializeSeed, EnumAccess, IntoDeserializer, MapAccess, SeqAccess, VariantAccess,
|
|
||||||
Visitor,
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
pub struct Deserializer<'de> {
|
|
||||||
// This string starts with the input data and characters are truncated off
|
|
||||||
// the beginning as data is parsed.
|
|
||||||
input: &'de str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserializer<'de> {
|
|
||||||
// By convention, `Deserializer` constructors are named like `from_xyz`.
|
|
||||||
// That way basic use cases are satisfied by something like
|
|
||||||
// `serde_json::from_str(...)` while advanced use cases that require a
|
|
||||||
// deserializer can make one with `serde_json::Deserializer::from_str(...)`.
|
|
||||||
pub fn from_str(input: &'de str) -> Self {
|
|
||||||
Deserializer { input }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// By convention, the public API of a Serde deserializer is one or more
|
|
||||||
// `from_xyz` methods such as `from_str`, `from_bytes`, or `from_reader`
|
|
||||||
// depending on what Rust types the deserializer is able to consume as input.
|
|
||||||
//
|
|
||||||
// This basic deserializer supports only `from_str`.
|
|
||||||
pub fn from_str<'a, T>(s: &'a str) -> Result<T>
|
|
||||||
where
|
|
||||||
T: Deserialize<'a>,
|
|
||||||
{
|
|
||||||
let mut deserializer = Deserializer::from_str(s);
|
|
||||||
let t = T::deserialize(&mut deserializer)?;
|
|
||||||
if deserializer.input.is_empty() {
|
|
||||||
Ok(t)
|
|
||||||
} else {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserializer<'de> {
|
|
||||||
fn peek(&mut self) -> Result<char> {
|
|
||||||
self.input.chars().next().ok_or(Error::Eof)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next(&mut self) -> Result<char> {
|
|
||||||
let ch = self.peek()?;
|
|
||||||
self.input = &self.input[ch.len_utf8()..];
|
|
||||||
Ok(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_bool(&mut self) -> Result<bool> {
|
|
||||||
let (value, size) = Boolean::peek_parse(self.input)?;
|
|
||||||
self.input = &self.input[size..];
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_unsigned<T>(&mut self) -> Result<T> {
|
|
||||||
self.parse_int(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_signed<T>(&mut self) -> Result<T> {
|
|
||||||
self.parse_int(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_int<T>(&mut self, positive_only: bool) -> Result<T> {
|
|
||||||
self.consume_whitespace()?;
|
|
||||||
|
|
||||||
match self.next()? {
|
|
||||||
c @ '0'..='9' => {
|
|
||||||
let mut significand = (c as u8 - b'0') as u64;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match self.peek()? {
|
|
||||||
c @ '0'..='9' => {
|
|
||||||
let digit = (c as u8 - b'0') as u64;
|
|
||||||
|
|
||||||
if significand.wrapping_mul(10).wrapping_add(digit) < u64::MAX {}
|
|
||||||
|
|
||||||
let _ = self.next();
|
|
||||||
significand = significand * 10 + digit;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// return self.parse_number(positive, significand);
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(Error::InvalidInteger),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_whitespace(&mut self) -> Result<()> {
|
|
||||||
loop {
|
|
||||||
match self.peek()? {
|
|
||||||
' ' | '\n' | '\t' | '\r' => {
|
|
||||||
let _ = self.next();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
match self.peek()? {
|
|
||||||
_ => todo!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_bool(self.parse_bool()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_i8(self.parse_signed()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_i16(self.parse_signed()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_i32(self.parse_signed()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_i64(self.parse_signed()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_u8(self.parse_unsigned()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_u16(self.parse_unsigned()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_u32(self.parse_unsigned()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_u64(self.parse_unsigned()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.deserialize_f64(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
unimplemented!("Cannot deserialize into a float value! Use a integer variant instead.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_char<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.deserialize_str(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.deserialize_str(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_unit()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_unit_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_unit()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_newtype_struct<V>(self, name: &'static str, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_newtype_struct(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.deserialize_seq(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_tuple_struct<V>(
|
|
||||||
self,
|
|
||||||
_name: &'static str,
|
|
||||||
_len: usize,
|
|
||||||
visitor: V,
|
|
||||||
) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.deserialize_seq(visitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
self.consume_whitespace()?;
|
|
||||||
Ok(visitor.visit_map(self)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_struct<V>(
|
|
||||||
self,
|
|
||||||
_name: &'static str,
|
|
||||||
_fields: &'static [&'static str],
|
|
||||||
visitor: V,
|
|
||||||
) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
visitor.visit_map(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_enum<V>(
|
|
||||||
self,
|
|
||||||
name: &'static str,
|
|
||||||
variants: &'static [&'static str],
|
|
||||||
visitor: V,
|
|
||||||
) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: Visitor<'de>,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, 'a> MapAccess<'de> for Deserializer<'de> {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
|
|
||||||
where
|
|
||||||
K: DeserializeSeed<'de>,
|
|
||||||
{
|
|
||||||
// A map section is ended when another section begins or we hit EOL.
|
|
||||||
// Therefore, we only check if a next section begins or in the case of
|
|
||||||
// EOL indicate that we're done.
|
|
||||||
if self.peek().unwrap_or('[') == '[' {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
seed.deserialize(self).map(Some)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
|
|
||||||
where
|
|
||||||
V: DeserializeSeed<'de>,
|
|
||||||
{
|
|
||||||
seed.deserialize(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod deserialize {
|
|
||||||
use crate::from_str;
|
|
||||||
use serde_derive::Deserialize;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unit() {
|
|
||||||
#[derive(Deserialize, PartialEq, Debug)]
|
|
||||||
struct Test;
|
|
||||||
assert_eq!(Test, from_str("").unwrap());
|
|
||||||
assert_eq!((), from_str("").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn float() {
|
|
||||||
from_str::<f64>("").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic() {
|
|
||||||
#[derive(Deserialize, PartialEq, Debug)]
|
|
||||||
struct Config {
|
|
||||||
user: User,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, PartialEq, Debug)]
|
|
||||||
struct User {
|
|
||||||
email: String,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected = Config {
|
|
||||||
user: User {
|
|
||||||
email: "code@eddie.sh".to_string(),
|
|
||||||
name: "Edward Shen".to_string(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
expected,
|
|
||||||
from_str("[user]\nemail=code@eddie.sh\nname=Edward Shen\n").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
42
src/error.rs
42
src/error.rs
|
@ -1,42 +0,0 @@
|
||||||
use std::fmt::{self, Display};
|
|
||||||
|
|
||||||
use serde::{de, ser};
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
// This is a bare-bones implementation. A real library would provide additional
|
|
||||||
// information in its error type, for example the line and column at which the
|
|
||||||
// error occurred, the byte offset into the input, or the current key being
|
|
||||||
// processed.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub enum Error {
|
|
||||||
Message(String),
|
|
||||||
Eof,
|
|
||||||
InvalidInteger,
|
|
||||||
InvalidBoolean(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ser::Error for Error {
|
|
||||||
fn custom<T: Display>(msg: T) -> Self {
|
|
||||||
Error::Message(msg.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl de::Error for Error {
|
|
||||||
fn custom<T: Display>(msg: T) -> Self {
|
|
||||||
Error::Message(msg.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Error {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Error::Message(msg) => formatter.write_str(msg),
|
|
||||||
Error::Eof => formatter.write_str("unexpected end of input"),
|
|
||||||
Error::InvalidInteger => formatter.write_str("invalid integer given"),
|
|
||||||
Error::InvalidBoolean(_) => formatter.write_str("invalid boolean given"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
2557
src/file.rs
Normal file
2557
src/file.rs
Normal file
File diff suppressed because it is too large
Load diff
70
src/lib.rs
70
src/lib.rs
|
@ -1,10 +1,70 @@
|
||||||
// mod de;
|
#![forbid(unsafe_code)]
|
||||||
pub mod config;
|
#![deny(missing_docs)]
|
||||||
mod error;
|
#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||||
// mod ser;
|
|
||||||
|
//! # `git_config`
|
||||||
|
//!
|
||||||
|
//! This crate is a high performance `git-config` file reader and writer. It
|
||||||
|
//! exposes a high level API to parse, read, and write [`git-config` files],
|
||||||
|
//! which are loosely based on the [INI file format].
|
||||||
|
//!
|
||||||
|
//! This crate has a few primary offerings and various accessory functions. The
|
||||||
|
//! table below gives a brief explanation of all offerings, loosely in order
|
||||||
|
//! from the highest to lowest abstraction.
|
||||||
|
//!
|
||||||
|
//! | Offering | Description | Zero-copy? |
|
||||||
|
//! | ------------- | --------------------------------------------------- | ----------------- |
|
||||||
|
//! | [`GitConfig`] | Accelerated wrapper for reading and writing values. | On some reads[^1] |
|
||||||
|
//! | [`Parser`] | Syntactic event emitter for `git-config` files. | Yes |
|
||||||
|
//! | [`Value`] | Wrappers for `git-config` value types. | Yes |
|
||||||
|
//!
|
||||||
|
//! This crate also exposes efficient value normalization which unescapes
|
||||||
|
//! characters and removes quotes through the `normalize_*` family of functions,
|
||||||
|
//! located in the [`values`] module.
|
||||||
|
//!
|
||||||
|
//! # Zero-copy versus zero-alloc
|
||||||
|
//!
|
||||||
|
//! We follow [`nom`]'s definition of "zero-copy":
|
||||||
|
//!
|
||||||
|
//! > If a parser returns a subset of its input data, it will return a slice of
|
||||||
|
//! > that input, without copying.
|
||||||
|
//!
|
||||||
|
//! Due to the syntax of `git-config`, we must allocate at the parsing level
|
||||||
|
//! (and thus higher level abstractions must allocate as well) in order to
|
||||||
|
//! provide a meaningful event stream. That being said, all operations with the
|
||||||
|
//! parser is still zero-copy. Higher level abstractions may have operations
|
||||||
|
//! that are zero-copy, but are not guaranteed to do so.
|
||||||
|
//!
|
||||||
|
//! However, we intend to be performant as possible, so allocations are
|
||||||
|
//! limited restricted and we attempt to avoid copying whenever possible.
|
||||||
|
//!
|
||||||
|
//! [^1]: When read values do not need normalization.
|
||||||
|
//!
|
||||||
|
//! [`git-config` files]: https://git-scm.com/docs/git-config#_configuration_file
|
||||||
|
//! [INI file format]: https://en.wikipedia.org/wiki/INI_file
|
||||||
|
//! [`GitConfig`]: crate::file::GitConfig
|
||||||
|
//! [`Parser`]: crate::parser::Parser
|
||||||
|
//! [`Value`]: crate::values::Value
|
||||||
|
//! [`values`]: crate::values
|
||||||
|
//! [`nom`]: https://github.com/Geal/nom
|
||||||
|
|
||||||
|
// Cargo.toml cannot have self-referential dependencies, so you can't just
|
||||||
|
// specify the actual serde crate when you define a feature called serde. We
|
||||||
|
// instead call the serde crate as serde_crate and then rename the crate to
|
||||||
|
// serde, to get around this in an intuitive manner.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
extern crate serde_crate as serde;
|
||||||
|
|
||||||
|
pub mod file;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod values;
|
pub mod values;
|
||||||
|
|
||||||
|
// mod de;
|
||||||
|
// mod ser;
|
||||||
|
// mod error;
|
||||||
// pub use de::{from_str, Deserializer};
|
// pub use de::{from_str, Deserializer};
|
||||||
pub use error::{Error, Result};
|
// pub use error::{Error, Result};
|
||||||
// pub use ser::{to_string, Serializer};
|
// pub use ser::{to_string, Serializer};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test_util;
|
||||||
|
|
1861
src/parser.rs
1861
src/parser.rs
File diff suppressed because it is too large
Load diff
184
src/ser.rs
184
src/ser.rs
|
@ -1,184 +0,0 @@
|
||||||
use crate::error::{Error, Result};
|
|
||||||
use ser::SerializeSeq;
|
|
||||||
use serde::{de::MapAccess, ser, Serialize};
|
|
||||||
|
|
||||||
pub struct Serializer {
|
|
||||||
output: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_string<T>(value: &T) -> Result<String>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
let mut serializer = Serializer {
|
|
||||||
output: String::new(),
|
|
||||||
};
|
|
||||||
value.serialize(&mut serializer)?;
|
|
||||||
Ok(serializer.output)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ser::Serializer for &'a mut Serializer {
|
|
||||||
type Ok = ();
|
|
||||||
type Error = Error;
|
|
||||||
type SerializeSeq = Self;
|
|
||||||
type SerializeTuple = Self;
|
|
||||||
type SerializeTupleStruct = Self;
|
|
||||||
type SerializeTupleVariant = Self;
|
|
||||||
type SerializeMap = Self;
|
|
||||||
type SerializeStruct = Self;
|
|
||||||
type SerializeStructVariant = Self;
|
|
||||||
|
|
||||||
fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
|
|
||||||
self.output += if v { "true" } else { "false" };
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_i8(self, v: i8) -> Result<Self::Ok> {
|
|
||||||
self.serialize_i64(i64::from(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_i16(self, v: i16) -> Result<Self::Ok> {
|
|
||||||
self.serialize_i64(i64::from(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_i32(self, v: i32) -> Result<Self::Ok> {
|
|
||||||
self.serialize_i64(i64::from(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_i64(self, v: i64) -> Result<Self::Ok> {
|
|
||||||
self.output += &v.to_string();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_u8(self, v: u8) -> Result<Self::Ok> {
|
|
||||||
self.serialize_u64(u64::from(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_u16(self, v: u16) -> Result<Self::Ok> {
|
|
||||||
self.serialize_u64(u64::from(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_u32(self, v: u32) -> Result<Self::Ok> {
|
|
||||||
self.serialize_u64(u64::from(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_u64(self, v: u64) -> Result<Self::Ok> {
|
|
||||||
self.output += &v.to_string();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_f32(self, v: f32) -> Result<Self::Ok> {
|
|
||||||
self.serialize_f64(f64::from(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_f64(self, v: f64) -> Result<Self::Ok> {
|
|
||||||
self.output += &v.to_string();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_char(self, v: char) -> Result<Self::Ok> {
|
|
||||||
self.output += &v.to_string();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_str(self, v: &str) -> Result<Self::Ok> {
|
|
||||||
self.output += v;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_none(self) -> Result<Self::Ok> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_some<T: ?Sized>(self, value: &T) -> Result<Self::Ok>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_unit(self) -> Result<Self::Ok> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_unit_variant(
|
|
||||||
self,
|
|
||||||
name: &'static str,
|
|
||||||
variant_index: u32,
|
|
||||||
variant: &'static str,
|
|
||||||
) -> Result<Self::Ok> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_newtype_struct<T: ?Sized>(self, name: &'static str, value: &T) -> Result<Self::Ok>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_newtype_variant<T: ?Sized>(
|
|
||||||
self,
|
|
||||||
name: &'static str,
|
|
||||||
variant_index: u32,
|
|
||||||
variant: &'static str,
|
|
||||||
value: &T,
|
|
||||||
) -> Result<Self::Ok>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
|
|
||||||
self.serialize_seq(Some(len))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_tuple_struct(
|
|
||||||
self,
|
|
||||||
name: &'static str,
|
|
||||||
len: usize,
|
|
||||||
) -> Result<Self::SerializeTupleStruct> {
|
|
||||||
self.serialize_seq(Some(len))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_tuple_variant(
|
|
||||||
self,
|
|
||||||
name: &'static str,
|
|
||||||
variant_index: u32,
|
|
||||||
variant: &'static str,
|
|
||||||
len: usize,
|
|
||||||
) -> Result<Self::SerializeTupleVariant> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_struct(self, name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_struct_variant(
|
|
||||||
self,
|
|
||||||
name: &'static str,
|
|
||||||
variant_index: u32,
|
|
||||||
variant: &'static str,
|
|
||||||
len: usize,
|
|
||||||
) -> Result<Self::SerializeStructVariant> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
75
src/test_util.rs
Normal file
75
src/test_util.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
//! This module is only included for tests, and contains common unit test helper
|
||||||
|
//! functions.
|
||||||
|
|
||||||
|
use crate::parser::{Event, Key, ParsedComment, ParsedSectionHeader};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub fn section_header_event(
|
||||||
|
name: &str,
|
||||||
|
subsection: impl Into<Option<(&'static str, &'static str)>>,
|
||||||
|
) -> Event<'_> {
|
||||||
|
Event::SectionHeader(section_header(name, subsection))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_header(
|
||||||
|
name: &str,
|
||||||
|
subsection: impl Into<Option<(&'static str, &'static str)>>,
|
||||||
|
) -> ParsedSectionHeader<'_> {
|
||||||
|
let name = name.into();
|
||||||
|
if let Some((separator, subsection_name)) = subsection.into() {
|
||||||
|
ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
separator: Some(Cow::Borrowed(separator)),
|
||||||
|
subsection_name: Some(Cow::Borrowed(subsection_name)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
separator: None,
|
||||||
|
subsection_name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn name_event(name: &'static str) -> Event<'static> {
|
||||||
|
Event::Key(Key(Cow::Borrowed(name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn value_event(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Value(Cow::Borrowed(value.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn value_not_done_event(value: &'static str) -> Event<'static> {
|
||||||
|
Event::ValueNotDone(Cow::Borrowed(value.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn value_done_event(value: &'static str) -> Event<'static> {
|
||||||
|
Event::ValueDone(Cow::Borrowed(value.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn newline_event() -> Event<'static> {
|
||||||
|
newline_custom_event("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn newline_custom_event(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Newline(Cow::Borrowed(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn whitespace_event(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Whitespace(Cow::Borrowed(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn comment_event(tag: char, msg: &'static str) -> Event<'static> {
|
||||||
|
Event::Comment(comment(tag, msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn comment(comment_tag: char, comment: &'static str) -> ParsedComment<'static> {
|
||||||
|
ParsedComment {
|
||||||
|
comment_tag,
|
||||||
|
comment: Cow::Borrowed(comment.as_bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fully_consumed<T>(t: T) -> (&'static [u8], T) {
|
||||||
|
(&[], t)
|
||||||
|
}
|
1264
src/values.rs
1264
src/values.rs
File diff suppressed because it is too large
Load diff
127
tests/integration_tests/file_integeration_test.rs
Normal file
127
tests/integration_tests/file_integeration_test.rs
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
use git_config::file::GitConfig;
|
||||||
|
use git_config::values::*;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
/// Asserts we can cast into all variants of our type
|
||||||
|
#[test]
|
||||||
|
fn get_value_for_all_provided_values() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = r#"
|
||||||
|
[core]
|
||||||
|
bool-explicit = false
|
||||||
|
bool-implicit
|
||||||
|
integer-no-prefix = 10
|
||||||
|
integer-prefix = 10g
|
||||||
|
color = brightgreen red \
|
||||||
|
bold
|
||||||
|
other = hello world
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let file = GitConfig::try_from(config)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Boolean>("core", None, "bool-explicit")?,
|
||||||
|
Boolean::False(Cow::Borrowed("false"))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Boolean>("core", None, "bool-implicit")?,
|
||||||
|
Boolean::True(TrueVariant::Implicit)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Integer>("core", None, "integer-no-prefix")?,
|
||||||
|
Integer {
|
||||||
|
value: 10,
|
||||||
|
suffix: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Integer>("core", None, "integer-no-prefix")?,
|
||||||
|
Integer {
|
||||||
|
value: 10,
|
||||||
|
suffix: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Integer>("core", None, "integer-prefix")?,
|
||||||
|
Integer {
|
||||||
|
value: 10,
|
||||||
|
suffix: Some(IntegerSuffix::Gibi),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Color>("core", None, "color")?,
|
||||||
|
Color {
|
||||||
|
foreground: Some(ColorValue::BrightGreen),
|
||||||
|
background: Some(ColorValue::Red),
|
||||||
|
attributes: vec![ColorAttribute::Bold]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Value>("core", None, "other")?,
|
||||||
|
Value::Other(Cow::Borrowed(b"hello world"))
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// There was a regression where lookup would fail because we only checked the
|
||||||
|
/// last section entry for any given section and subsection
|
||||||
|
#[test]
|
||||||
|
fn get_value_looks_up_all_sections_before_failing() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = r#"
|
||||||
|
[core]
|
||||||
|
bool-explicit = false
|
||||||
|
bool-implicit = false
|
||||||
|
[core]
|
||||||
|
bool-implicit
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let file = GitConfig::try_from(config)?;
|
||||||
|
|
||||||
|
// Checks that we check the last entry first still
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Boolean>("core", None, "bool-implicit")?,
|
||||||
|
Boolean::True(TrueVariant::Implicit)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Boolean>("core", None, "bool-explicit")?,
|
||||||
|
Boolean::False(Cow::Borrowed("false"))
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn section_names_are_case_insensitive() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = "[core] bool-implicit";
|
||||||
|
let file = GitConfig::try_from(config)?;
|
||||||
|
assert!(file.value::<Boolean>("core", None, "bool-implicit").is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Boolean>("core", None, "bool-implicit"),
|
||||||
|
file.value::<Boolean>("CORE", None, "bool-implicit")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_names_are_case_insensitive() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = "[core]
|
||||||
|
a = true
|
||||||
|
A = false";
|
||||||
|
let file = GitConfig::try_from(config)?;
|
||||||
|
assert_eq!(file.multi_value::<Boolean>("core", None, "a")?.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
file.value::<Boolean>("core", None, "a"),
|
||||||
|
file.value::<Boolean>("core", None, "A")
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
8
tests/integration_tests/main.rs
Normal file
8
tests/integration_tests/main.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// See https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html
|
||||||
|
// for an explanation of why the integration tests are laid out like this.
|
||||||
|
//
|
||||||
|
// TL;DR single mod makes integration tests faster to compile, test, and with
|
||||||
|
// less build artifacts.
|
||||||
|
|
||||||
|
mod file_integeration_test;
|
||||||
|
mod parser_integration_tests;
|
227
tests/integration_tests/parser_integration_tests.rs
Normal file
227
tests/integration_tests/parser_integration_tests.rs
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
use git_config::parser::{parse_from_str, Event, Key, ParsedSectionHeader, SectionHeaderName};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub fn section_header_event(
|
||||||
|
name: &str,
|
||||||
|
subsection: impl Into<Option<(&'static str, &'static str)>>,
|
||||||
|
) -> Event<'_> {
|
||||||
|
Event::SectionHeader(section_header(name, subsection))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_header(
|
||||||
|
name: &str,
|
||||||
|
subsection: impl Into<Option<(&'static str, &'static str)>>,
|
||||||
|
) -> ParsedSectionHeader<'_> {
|
||||||
|
let name = SectionHeaderName(Cow::Borrowed(name));
|
||||||
|
if let Some((separator, subsection_name)) = subsection.into() {
|
||||||
|
ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
separator: Some(Cow::Borrowed(separator)),
|
||||||
|
subsection_name: Some(Cow::Borrowed(subsection_name)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParsedSectionHeader {
|
||||||
|
name,
|
||||||
|
separator: None,
|
||||||
|
subsection_name: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(name: &'static str) -> Event<'static> {
|
||||||
|
Event::Key(Key(Cow::Borrowed(name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Value(Cow::Borrowed(value.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newline() -> Event<'static> {
|
||||||
|
newline_custom("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newline_custom(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Newline(Cow::Borrowed(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn whitespace(value: &'static str) -> Event<'static> {
|
||||||
|
Event::Whitespace(Cow::Borrowed(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn separator() -> Event<'static> {
|
||||||
|
Event::KeyValueSeparator
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn personal_config() {
|
||||||
|
let config = r#"[user]
|
||||||
|
email = code@eddie.sh
|
||||||
|
name = Foo Bar
|
||||||
|
[core]
|
||||||
|
autocrlf = input
|
||||||
|
[push]
|
||||||
|
default = simple
|
||||||
|
[commit]
|
||||||
|
gpgsign = true
|
||||||
|
[gpg]
|
||||||
|
program = gpg
|
||||||
|
[url "ssh://git@github.com/"]
|
||||||
|
insteadOf = "github://"
|
||||||
|
[url "ssh://git@git.eddie.sh/edward/"]
|
||||||
|
insteadOf = "gitea://"
|
||||||
|
[pull]
|
||||||
|
ff = only
|
||||||
|
[init]
|
||||||
|
defaultBranch = master"#;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_from_str(config)
|
||||||
|
.unwrap()
|
||||||
|
.into_vec(),
|
||||||
|
vec![
|
||||||
|
section_header_event("user", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("email"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("code@eddie.sh"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("name"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("Foo Bar"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("core", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("autocrlf"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("input"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("push", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("default"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("simple"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("commit", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("gpgsign"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("true"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("gpg", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("program"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("gpg"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("url", (" ", "ssh://git@github.com/")),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("insteadOf"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("\"github://\""),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("url", (" ", "ssh://git@git.eddie.sh/edward/")),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("insteadOf"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("\"gitea://\""),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("pull", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("ff"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("only"),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
section_header_event("init", None),
|
||||||
|
newline(),
|
||||||
|
|
||||||
|
whitespace(" "),
|
||||||
|
name("defaultBranch"),
|
||||||
|
whitespace(" "),
|
||||||
|
separator(),
|
||||||
|
whitespace(" "),
|
||||||
|
value("master"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty() {
|
||||||
|
assert_eq!(parse_from_str("").unwrap().into_vec(), vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_whitespace() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_from_str("\n \n \n").unwrap().into_vec(),
|
||||||
|
vec![
|
||||||
|
newline(),
|
||||||
|
whitespace(" "),
|
||||||
|
newline(),
|
||||||
|
whitespace(" "),
|
||||||
|
newline(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newline_events_are_merged() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_from_str("\n\n\n\n\n").unwrap().into_vec(),
|
||||||
|
vec![newline_custom("\n\n\n\n\n")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error() {
|
||||||
|
let input = "[core] a=b\n 4a=3";
|
||||||
|
println!("{}", parse_from_str(input).unwrap_err());
|
||||||
|
let input = "[core] a=b\n =3";
|
||||||
|
println!("{}", parse_from_str(input).unwrap_err());
|
||||||
|
let input = "[core";
|
||||||
|
println!("{}", parse_from_str(input).unwrap_err());
|
||||||
|
}
|
|
@ -1,82 +0,0 @@
|
||||||
use serde_git_config::parser::{parse_from_str, Event, ParsedSectionHeader, Parser};
|
|
||||||
use serde_git_config::values::Value;
|
|
||||||
|
|
||||||
fn fully_consumed<T>(t: T) -> (&'static str, T) {
|
|
||||||
("", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn section_header(name: &'static str, subname: impl Into<Option<&'static str>>) -> Event<'static> {
|
|
||||||
Event::SectionHeader(ParsedSectionHeader {
|
|
||||||
name,
|
|
||||||
subsection_name: subname.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(name: &'static str) -> Event<'static> {
|
|
||||||
Event::Key(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn value(value: &'static str) -> Event<'static> {
|
|
||||||
Event::Value(Value::from_str(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn personal_config() {
|
|
||||||
let config = r#"[user]
|
|
||||||
email = code@eddie.sh
|
|
||||||
name = Edward Shen
|
|
||||||
[core]
|
|
||||||
autocrlf = input
|
|
||||||
[push]
|
|
||||||
default = simple
|
|
||||||
[commit]
|
|
||||||
gpgsign = true
|
|
||||||
[gpg]
|
|
||||||
program = gpg
|
|
||||||
[url "ssh://git@github.com/"]
|
|
||||||
insteadOf = "github://"
|
|
||||||
[url "ssh://git@git.eddie.sh/edward/"]
|
|
||||||
insteadOf = "gitea://"
|
|
||||||
[pull]
|
|
||||||
ff = only
|
|
||||||
[init]
|
|
||||||
defaultBranch = master"#;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
parse_from_str(config)
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![
|
|
||||||
section_header("user", None),
|
|
||||||
name("email"),
|
|
||||||
value("code@eddie.sh"),
|
|
||||||
name("name"),
|
|
||||||
value("Edward Shen"),
|
|
||||||
section_header("core", None),
|
|
||||||
name("autocrlf"),
|
|
||||||
value("input"),
|
|
||||||
section_header("push", None),
|
|
||||||
name("default"),
|
|
||||||
value("simple"),
|
|
||||||
section_header("commit", None),
|
|
||||||
name("gpgsign"),
|
|
||||||
value("true"),
|
|
||||||
section_header("gpg", None),
|
|
||||||
name("program"),
|
|
||||||
value("gpg"),
|
|
||||||
section_header("url", "ssh://git@github.com/"),
|
|
||||||
name("insteadOf"),
|
|
||||||
value("\"github://\""),
|
|
||||||
section_header("url", "ssh://git@git.eddie.sh/edward/"),
|
|
||||||
name("insteadOf"),
|
|
||||||
value("\"gitea://\""),
|
|
||||||
section_header("pull", None),
|
|
||||||
name("ff"),
|
|
||||||
value("only"),
|
|
||||||
section_header("init", None),
|
|
||||||
name("defaultBranch"),
|
|
||||||
value("master"),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
Reference in a new issue