Skip to content

Commit 94917ed

Browse files
authored
Yaml support (#14)
Adds support for YAML using [yaml_rust](https://crates.io/crates/yaml-rust). Some compromises due to YAML funniness: compound keys will be treated as their hashes.
1 parent 6b5be0e commit 94917ed

File tree

9 files changed

+348
-2
lines changed

9 files changed

+348
-2
lines changed

Cargo.lock

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ serde_json = "1.0"
1515
toml = "0.5"
1616
tracing = "0.1"
1717
tracing-subscriber = "0.2.18"
18+
yaml-rust = "0.4.5"

src/format.rs

+183-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ use ::toml as serde_toml;
1515
pub enum Format {
1616
Json,
1717
Toml,
18+
Yaml,
1819
}
1920

20-
pub const POSSIBLE_FORMATS: &[&str] = &["json", "toml"];
21+
pub const POSSIBLE_FORMATS: &[&str] = &["json", "toml", "yaml"];
2122

2223
impl std::fmt::Display for Format {
2324
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
@@ -27,6 +28,7 @@ impl std::fmt::Display for Format {
2728
match self {
2829
Format::Json => "json",
2930
Format::Toml => "toml",
31+
Format::Yaml => "yaml",
3032
}
3133
)
3234
}
@@ -48,6 +50,8 @@ impl FromStr for Format {
4850
Ok(Format::Json)
4951
} else if s == "toml" {
5052
Ok(Format::Toml)
53+
} else if s == "yaml" || s == "yml" {
54+
Ok(Format::Yaml)
5155
} else {
5256
Err(ParseFormatError::NoSuchFormat(s))
5357
}
@@ -78,6 +82,13 @@ impl Format {
7882
fs_from_value(v, &config, &mut inodes);
7983
info!("done");
8084
}
85+
Format::Yaml => {
86+
info!("reading toml value");
87+
let v = yaml::from_reader(reader).expect("YAML");
88+
info!("building inodes");
89+
fs_from_value(v, &config, &mut inodes);
90+
info!("done");
91+
}
8192
};
8293

8394
FS::new(inodes, config)
@@ -121,6 +132,14 @@ impl Format {
121132
toml::to_writer(writer, &v).unwrap();
122133
info!("done");
123134
}
135+
Format::Yaml => {
136+
info!("generating yaml value");
137+
let v: yaml::Value = value_from_fs(fs, fuser::FUSE_ROOT_ID);
138+
info!("writing");
139+
debug!("outputting {}", v);
140+
yaml::to_writer(writer, &v).unwrap();
141+
info!("done");
142+
}
124143
}
125144
}
126145
}
@@ -389,8 +408,8 @@ mod json {
389408

390409
mod toml {
391410
use super::*;
392-
393411
use serde_toml::Value;
412+
394413
#[derive(Debug)]
395414
pub enum Error<E> {
396415
Io(std::io::Error),
@@ -485,3 +504,165 @@ mod toml {
485504
}
486505
}
487506
}
507+
508+
mod yaml {
509+
use super::*;
510+
use std::hash::{Hash, Hasher};
511+
use yaml_rust::{EmitError, ScanError, Yaml};
512+
513+
#[derive(Clone, Debug)]
514+
pub struct Value(Yaml);
515+
516+
#[derive(Debug)]
517+
pub enum Error<E> {
518+
Io(std::io::Error),
519+
Yaml(E),
520+
}
521+
522+
pub fn from_reader(mut reader: Box<dyn std::io::Read>) -> Result<Value, Error<ScanError>> {
523+
let mut text = String::new();
524+
let _len = reader.read_to_string(&mut text).map_err(Error::Io)?;
525+
yaml_rust::YamlLoader::load_from_str(&text)
526+
.map(|vs| {
527+
Value(if vs.len() == 1 {
528+
vs.into_iter().next().unwrap()
529+
} else {
530+
Yaml::Array(vs)
531+
})
532+
})
533+
.map_err(Error::Yaml)
534+
}
535+
536+
pub fn to_writer(
537+
mut writer: Box<dyn std::io::Write>,
538+
v: &Value,
539+
) -> Result<(), Error<EmitError>> {
540+
let mut text = String::new();
541+
let mut emitter = yaml_rust::YamlEmitter::new(&mut text);
542+
emitter.dump(&v.0).map_err(Error::Yaml)?;
543+
writer.write_all(text.as_bytes()).map_err(Error::Io)
544+
}
545+
546+
impl std::fmt::Display for Value {
547+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
548+
let mut emitter = yaml_rust::YamlEmitter::new(f);
549+
emitter.dump(&self.0).map_err(|e| match e {
550+
yaml_rust::EmitError::FmtError(e) => e,
551+
yaml_rust::EmitError::BadHashmapKey => {
552+
panic!("unrecoverable YAML display error: BadHashmapKey")
553+
}
554+
})
555+
}
556+
}
557+
558+
fn yaml_size(v: &Yaml) -> usize {
559+
match v {
560+
Yaml::Real(_)
561+
| Yaml::Integer(_)
562+
| Yaml::String(_)
563+
| Yaml::Boolean(_)
564+
| Yaml::Null
565+
| Yaml::BadValue
566+
| Yaml::Alias(_) => 1,
567+
Yaml::Array(vs) => vs.iter().map(|v| yaml_size(v)).sum::<usize>() + 1,
568+
Yaml::Hash(fvs) => fvs.iter().map(|(_, v)| yaml_size(v)).sum::<usize>() + 1,
569+
}
570+
}
571+
572+
fn yaml_key_to_string(v: Yaml) -> String {
573+
match v {
574+
Yaml::Boolean(b) => format!("{}", b),
575+
Yaml::Real(s) => s,
576+
Yaml::Integer(n) => format!("{}", n),
577+
Yaml::String(s) => s,
578+
Yaml::Alias(n) => format!("alias{}", n),
579+
Yaml::Array(vs) => {
580+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
581+
vs.hash(&mut hasher);
582+
format!("{}", hasher.finish())
583+
}
584+
Yaml::Hash(fvs) => {
585+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
586+
fvs.hash(&mut hasher);
587+
format!("{}", hasher.finish())
588+
}
589+
Yaml::Null => "null".into(),
590+
Yaml::BadValue => "badvalue".into(),
591+
}
592+
}
593+
594+
impl Nodelike for Value {
595+
fn kind(&self) -> FileType {
596+
match &self.0 {
597+
Yaml::Array(_) | Yaml::Hash(_) => FileType::Directory,
598+
_ => FileType::RegularFile,
599+
}
600+
}
601+
602+
fn size(&self) -> usize {
603+
yaml_size(&self.0)
604+
}
605+
606+
fn node(self, config: &Config) -> Node<Self> {
607+
let nl = if config.add_newlines { "\n" } else { "" };
608+
609+
match self.0 {
610+
Yaml::Null => Node::String("".into()),
611+
Yaml::Boolean(b) => Node::Bytes(format!("{}{}", b, nl).into_bytes()),
612+
Yaml::Real(s) => Node::String(s + nl),
613+
Yaml::Integer(n) => Node::Bytes(format!("{}{}", n, nl).into_bytes()),
614+
Yaml::String(s) => {
615+
if config.try_decode_base64 {
616+
if let Ok(bytes) = base64::decode_config(&s, config.base64) {
617+
return Node::Bytes(bytes);
618+
}
619+
}
620+
621+
Node::String(if s.ends_with('\n') { s } else { s + nl })
622+
}
623+
Yaml::Array(vs) => Node::List(vs.into_iter().map(Value).collect()),
624+
Yaml::Hash(fvs) => Node::Map(
625+
fvs.into_iter()
626+
.map(|(k, v)| (yaml_key_to_string(k), Value(v)))
627+
.collect(),
628+
),
629+
// ??? 2021-06-21 support aliases w/hard links?
630+
Yaml::Alias(n) => Node::Bytes(format!("alias{}{}", n, nl).into_bytes()),
631+
Yaml::BadValue => Node::Bytes("bad YAML value".into()),
632+
}
633+
}
634+
635+
fn from_string(contents: String, _config: &Config) -> Self {
636+
if contents == "true" {
637+
Value(Yaml::Boolean(true))
638+
} else if contents == "false" {
639+
Value(Yaml::Boolean(false))
640+
} else if let Ok(n) = i64::from_str(&contents) {
641+
Value(Yaml::Integer(n))
642+
} else if f64::from_str(&contents).is_ok() {
643+
Value(Yaml::Real(contents))
644+
} else {
645+
Value(Yaml::String(contents))
646+
}
647+
}
648+
649+
fn from_bytes<T>(contents: T, config: &Config) -> Self
650+
where
651+
T: AsRef<[u8]>,
652+
{
653+
Value(Yaml::String(base64::encode_config(contents, config.base64)))
654+
}
655+
656+
fn from_list_dir(vs: Vec<Self>, _config: &Config) -> Self {
657+
Value(Yaml::Array(vs.into_iter().map(|v| v.0).collect()))
658+
}
659+
660+
fn from_named_dir(fvs: HashMap<String, Self>, config: &Config) -> Self {
661+
Value(Yaml::Hash(
662+
fvs.into_iter()
663+
.map(|(k, v)| (Value::from_string(k, config).0, v.0))
664+
.collect(),
665+
))
666+
}
667+
}
668+
}

tests/yaml_output.sh

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/bin/sh
2+
3+
fail() {
4+
echo FAILED: $1
5+
if [ "$MNT" ]
6+
then
7+
cd
8+
umount "$MNT"
9+
rmdir "$MNT"
10+
rm "$YAML"
11+
fi
12+
exit 1
13+
}
14+
15+
MNT=$(mktemp -d)
16+
YAML=$(mktemp)
17+
18+
mv "$YAML" "$YAML".yaml
19+
YAML="$YAML".yaml
20+
21+
cp ../yaml/invoice.yaml "$YAML"
22+
23+
ffs -i "$MNT" "$YAML" &
24+
PID=$!
25+
sleep 2
26+
case $(ls "$MNT") in
27+
(bill-to*comments*date*invoice*product*ship-to*tax*total) ;;
28+
(*) fail ls;;
29+
esac
30+
[ "$(cat $MNT/date)" = "2001-01-23" ] || fail date
31+
[ "$(cat $MNT/product/0/description)" = "Basketball" ] || fail product
32+
echo orange >"$MNT/product/0/color"
33+
echo pink >"$MNT/product/1/color"
34+
umount "$MNT" || fail unmount1
35+
sleep 1
36+
kill -0 $PID >/dev/null 2>&1 && fail process1
37+
38+
ffs --readonly --no-output "$MNT" "$YAML" &
39+
PID=$!
40+
sleep 2
41+
[ "$(cat $MNT/product/0/description)" = "Basketball" ] || fail desc1
42+
[ "$(cat $MNT/product/0/color)" = "orange" ] || fail color1
43+
[ "$(cat $MNT/product/1/description)" = "Super Hoop" ] || fail desc2
44+
[ "$(cat $MNT/product/1/color)" = "pink" ] || fail
45+
umount "$MNT" || fail unmount2
46+
sleep 1
47+
kill -0 $PID >/dev/null 2>&1 && fail process2
48+
49+
rmdir "$MNT" || fail mount
50+
rm "$YAML"

yaml/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mnt

yaml/alias.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
hr:
3+
- Mark McGwire
4+
# Following node labeled SS
5+
- &SS Sammy Sosa
6+
rbi:
7+
- *SS # Subsequent occurrence
8+
- Ken Griffey

yaml/build.yml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Main workflow
2+
3+
on:
4+
- pull_request
5+
- push
6+
7+
jobs:
8+
build:
9+
strategy:
10+
fail-fast: false
11+
matrix:
12+
os:
13+
- macos-latest
14+
- ubuntu-latest
15+
16+
runs-on: ${{ matrix.os }}
17+
18+
steps:
19+
- name: Install FUSE
20+
run: |
21+
if [ "$RUNNER_OS" = "Linux" ]; then
22+
sudo apt-get install fuse libfuse-dev pkg-config
23+
elif [ "$RUNNER_OS" = "macOS" ]; then
24+
brew install macfuse pkg-config
25+
else
26+
echo Unsupported RUNNER_OS=$RUNNER_OS
27+
exit 1
28+
fi
29+
30+
- name: Checkout code
31+
uses: actions/checkout@v2
32+
33+
- name: Build ffs and run unit tests
34+
run: |
35+
cargo build --verbose --all
36+
cargo test
37+
38+
- name: Integration tests
39+
run: |
40+
PATH="$(pwd)/target/debug:$PATH"
41+
RUST_LOG="ffs=info"
42+
export RUST_LOG
43+
cd tests
44+
for test in *.sh
45+
do
46+
echo ========== RUNNING TEST: $(basename ${test%*.sh})
47+
./${test} || exit 1
48+
done
49+

yaml/eg2.7.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Ranking of 1998 home runs
2+
---
3+
- Mark McGwire
4+
- Sammy Sosa
5+
- Ken Griffey
6+
7+
# Team ranking
8+
---
9+
- Chicago Cubs
10+
- St Louis Cardinals

0 commit comments

Comments
 (0)