Compare commits
No commits in common. "master" and "eacb903dfd030474b928baa4f5c8e4bfa72df24e" have entirely different histories.
master
...
eacb903dfd
23 changed files with 1648 additions and 5928 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
tarpaulin-report.html
|
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"Multivar",
|
||||
"autocrlf",
|
||||
"bstr",
|
||||
"combinator",
|
||||
"gitea",
|
||||
"gpgsign",
|
||||
"implicits",
|
||||
"multivars",
|
||||
"subname"
|
||||
]
|
||||
}
|
27
Cargo.toml
27
Cargo.toml
|
@ -1,31 +1,14 @@
|
|||
[package]
|
||||
name = "git-config"
|
||||
name = "serde-git-config"
|
||||
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>"]
|
||||
edition = "2018"
|
||||
keywords = ["git-config", "git", "config", "gitoxide"]
|
||||
categories = ["config", "parser-implementations"]
|
||||
exclude = ["fuzz/**/*", ".vscode/**/*", "benches/**/*"]
|
||||
|
||||
[features]
|
||||
# serde = ["serde_crate"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
memchr = "2"
|
||||
nom = { version = "6", default_features = false, features = ["std"] }
|
||||
serde_crate = { version = "1", package = "serde", optional = true }
|
||||
serde = "1.0"
|
||||
nom = "6"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_derive = "1.0"
|
||||
criterion = "0.3"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
[[bench]]
|
||||
name = "large_config_file"
|
||||
harness = false
|
||||
serde_derive = "1.0"
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-APACHE
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-MIT
|
75
README.md
75
README.md
|
@ -1,75 +0,0 @@
|
|||
# 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
|
|
@ -1,293 +0,0 @@
|
|||
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
4
fuzz/.gitignore
vendored
|
@ -1,4 +0,0 @@
|
|||
|
||||
target
|
||||
corpus
|
||||
artifacts
|
|
@ -1,26 +0,0 @@
|
|||
|
||||
[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
|
|
@ -1,9 +0,0 @@
|
|||
#![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
Normal file
221
src/config.rs
Normal file
|
@ -0,0 +1,221 @@
|
|||
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
Normal file
410
src/de.rs
Normal file
|
@ -0,0 +1,410 @@
|
|||
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
Normal file
42
src/error.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
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
2557
src/file.rs
File diff suppressed because it is too large
Load diff
70
src/lib.rs
70
src/lib.rs
|
@ -1,70 +1,10 @@
|
|||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
#![warn(clippy::pedantic, clippy::nursery, clippy::cargo)]
|
||||
|
||||
//! # `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;
|
||||
// mod de;
|
||||
pub mod config;
|
||||
mod error;
|
||||
// mod ser;
|
||||
pub mod parser;
|
||||
pub mod values;
|
||||
|
||||
// mod de;
|
||||
// mod ser;
|
||||
// mod error;
|
||||
// pub use de::{from_str, Deserializer};
|
||||
// pub use error::{Error, Result};
|
||||
pub use error::{Error, Result};
|
||||
// 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
Normal file
184
src/ser.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
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!()
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
//! 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)
|
||||
}
|
1262
src/values.rs
1262
src/values.rs
File diff suppressed because it is too large
Load diff
|
@ -1,127 +0,0 @@
|
|||
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(())
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// 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;
|
|
@ -1,227 +0,0 @@
|
|||
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());
|
||||
}
|
82
tests/parser_integration_tests.rs
Normal file
82
tests/parser_integration_tests.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
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