Compare commits

..

No commits in common. "9c9f7cfa82750a70471f3bdc97d4d685d4219999" and "74e2da2f2941366e76a24bb0a7d63e29d7cb49ba" have entirely different histories.

3 changed files with 77 additions and 480 deletions

View file

@ -109,15 +109,14 @@ If **FILE** is **-**, this reads text from stdin instead.
The following is an example of a *config.toml* file: The following is an example of a *config.toml* file:
``` ```
auto=true # should `fwd` should enable identified ports (default true) auto=true # should `fwd` should enable identified ports
[servers.foo] # Server-specific settings for foo [servers.foo] # Server-specific settings for foo
auto=true # defaults to the global setting auto=true
ports=[1080, 1082] # ports that are always present ports=[1080, 1082] # ports that are always present
[servers.bar.ports] # `ports` can also be a table with port numbers as keys [servers.bar.ports] # `ports` can also be a table with port numbers as keys
1080=true # the values can be booleans (for enabled)... 1080=true # the values can be booleans (for enabled)
1081="My program" # or strings (for descriptions).
[servers.bar.ports.1082] # port values can also be tables [servers.bar.ports.1082] # port values can also be tables
enabled=true enabled=true

View file

@ -1,128 +0,0 @@
"""A script to automate building and uploading a release archive.
This is in python instead of bash because I abhor bash. Even though it's a
little nicer for running commands, it's worse at everything else.
"""
import dataclasses
import os
import os.path
import pathlib
import shutil
import subprocess
RELEASE_TAG = os.getenv("RELEASE_TAG")
BUILD = os.getenv("BUILD")
if BUILD is None:
raise Exception("you *must* set the BUILD environment variable")
@dataclasses.dataclass
class BuildSettings:
target: str
test: bool = True
man_page: bool = True
strip: bool = True
windows: bool = False
ext: str = ""
print(f"doing release: {BUILD}")
build = {
"linux": BuildSettings(
target="x86_64-unknown-linux-musl",
),
"macos": BuildSettings(
target="x86_64-apple-darwin",
),
"arm-macos": BuildSettings(
target="aarch64-apple-darwin",
),
"windows": BuildSettings(
target="x86_64-pc-windows-msvc",
strip=False,
man_page=False,
windows=True,
ext=".exe",
),
}[BUILD]
print(f"settings: {build}")
target_dir = pathlib.Path("target") / build.target / "release"
bins = [(target_dir / bin).with_suffix(build.ext) for bin in ["fwd", "fwd-browse"]]
def build_and_test(staging: pathlib.Path):
# Tools
subprocess.run(
["rustup", "target", "add", build.target],
check=True,
)
# Test...?
if build.test:
subprocess.run(
["cargo", "test", "--verbose", "--release", "--target", build.target],
check=True,
)
# Build
subprocess.run(
["cargo", "build", "--verbose", "--release", "--target", build.target],
check=True,
)
# Strip
if build.strip:
for bin in bins:
subprocess.run(["strip", bin], check=True)
# Copy
for bin in bins:
shutil.copyfile(bin, os.path.join(staging, os.path.basename(bin)))
def build_docs(staging: pathlib.Path):
shutil.copyfile("README.md", staging / "README.md")
if build.man_page:
print("Creating man page...")
proc = subprocess.run(
["pandoc", "-s", "-tman", os.path.join("doc", "fwd.man.md")],
check=True,
capture_output=True,
encoding="utf8",
)
contents = proc.stdout
with open(staging / "fwd.1", "w", encoding="utf-8") as f:
f.write(contents)
staging = pathlib.Path(f"fwd-{build.target}")
os.makedirs(staging, exist_ok=True)
build_and_test(staging)
build_docs(staging)
print("Creating archive...")
if build.windows:
archive = f"{staging}.zip"
subprocess.run(["7z", "a", archive, f"{staging}"], check=True)
else:
archive = f"{staging}.tar.gz"
subprocess.run(["tar", "czf", archive, f"{staging}"], check=True)
shutil.rmtree(staging)
if RELEASE_TAG is None:
print("Not releasing to github, RELEASE_TAG is none.")
else:
print(f"Uploading {archive} to github release {RELEASE_TAG}...")
subprocess.run(
["gh", "release", "upload", RELEASE_TAG, archive, "--clobber"],
check=True,
)
os.unlink(archive)

View file

@ -1,17 +1,15 @@
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use std::collections::hash_map; use std::collections::hash_map;
use std::collections::HashMap; use std::collections::HashMap;
use toml::value::{Table, Value}; use toml::Value;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PortConfig { pub struct PortConfig {
pub enabled: bool, pub enabled: bool,
pub description: Option<String>, pub description: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ServerConfig { pub struct ServerConfig {
auto: bool, auto: bool,
ports: HashMap<u16, PortConfig>, ports: HashMap<u16, PortConfig>,
@ -47,7 +45,6 @@ impl ServerConfig {
} }
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Config { pub struct Config {
auto: bool, auto: bool,
servers: HashMap<String, ServerConfig>, servers: HashMap<String, ServerConfig>,
@ -88,101 +85,85 @@ fn default() -> Config {
Config { auto: true, servers: HashMap::new() } Config { auto: true, servers: HashMap::new() }
} }
fn get_bool(table: &Table, key: &str, default: bool) -> Result<bool> {
match table.get(key) {
None => Ok(default),
Some(Value::Boolean(v)) => Ok(*v),
Some(v) => bail!("expected a true or false, got {v:?}"),
}
}
fn parse_config(value: &Value) -> Result<Config> { fn parse_config(value: &Value) -> Result<Config> {
let Value::Table(table) = value else { match value {
bail!("top level must be a table") Value::Table(table) => Ok({
let auto = match table.get("auto") {
None => true,
Some(Value::Boolean(v)) => *v,
Some(v) => bail!("expected a true or false, got {:?}", v),
}; };
Config { auto, servers: get_servers(table, auto)? }
let auto = get_bool(table, "auto", true)?; }),
let servers = match table.get("servers") { _ => bail!("top level must be a table"),
None => &Table::new(), }
Some(Value::Table(t)) => t,
Some(v) => bail!("Expected a table in the servers key, got {v:?}"),
};
Ok(Config {
auto,
servers: parse_servers(servers, auto)?,
})
} }
fn parse_servers( fn get_servers(
table: &Table, table: &toml::value::Table,
auto: bool, auto: bool,
) -> Result<HashMap<String, ServerConfig>> { ) -> Result<HashMap<String, ServerConfig>> {
match table.get("servers") {
None => Ok(HashMap::new()),
Some(Value::Table(table)) => Ok({
let mut servers = HashMap::new(); let mut servers = HashMap::new();
for (k, v) in table { for (k, v) in table {
let Value::Table(table) = v else { servers.insert(k.clone(), get_server(v, auto)?);
bail!("expected a table for server {k}, got {v:?}"); }
}; servers
}),
servers.insert(k.clone(), parse_server(table, auto)?); v => bail!("expected a table in the servers key, got {:?}", v),
} }
Ok(servers)
} }
fn parse_server(table: &Table, auto: bool) -> Result<ServerConfig> { fn get_server(value: &Value, auto: bool) -> Result<ServerConfig> {
let auto = get_bool(table, "auto", auto)?;
let ports = match table.get("ports") {
None => HashMap::new(),
Some(v) => parse_ports(v)?,
};
Ok(ServerConfig { auto, ports })
}
fn parse_ports(value: &Value) -> Result<HashMap<u16, PortConfig>> {
match value { match value {
Value::Array(array) => { Value::Table(table) => Ok(ServerConfig {
let mut ports = HashMap::new(); auto: match table.get("auto") {
for v in array { None => auto, // Default to global default
ports.insert( Some(Value::Boolean(v)) => *v,
get_port_number(v)?, Some(v) => bail!("expected true or false, got {:?}", v),
PortConfig { enabled: true, description: None }, },
); ports: get_ports(table)?,
}),
value => bail!("expected a table, got {:?}", value),
} }
Ok(ports)
} }
Value::Table(table) => { fn get_ports(table: &toml::value::Table) -> Result<HashMap<u16, PortConfig>> {
match table.get("ports") {
None => Ok(HashMap::new()),
Some(Value::Table(table)) => Ok({
let mut ports = HashMap::new(); let mut ports = HashMap::new();
for (k,v) in table { for (k,v) in table {
let port:u16 = k.parse()?; let port:u16 = k.parse()?;
let config = parse_port_config(v)?; let config = match v {
Value::Boolean(enabled) => PortConfig{enabled:*enabled, description:None},
Value::Table(table) => PortConfig{
enabled: match table.get("enabled") {
Some(Value::Boolean(enabled)) => *enabled,
_ => bail!("not implemented"),
},
description: match table.get("description") {
Some(Value::String(desc)) => Some(desc.clone()),
Some(v) => bail!("expect a string description, got {:?}", v),
None => None,
},
},
_ => bail!("expected either a boolean (enabled) or a table for a port config, got {:?}", v),
};
ports.insert(port, config); ports.insert(port, config);
} }
Ok(ports) ports
}
_ => bail!("ports must be either an array or a table, got {value:?}"),
}
}
fn parse_port_config(value: &Value) -> Result<PortConfig> {
match value {
Value::Boolean(enabled) => Ok(PortConfig{enabled:*enabled, description:None}),
Value::String(description) => Ok(PortConfig{
enabled: true,
description: Some(description.clone()),
}), }),
Value::Table(table) => { Some(Value::Array(array)) => Ok({
let enabled = get_bool(table, "enabled", true)?; let mut ports = HashMap::new();
let description = match table.get("description") { for v in array {
Some(Value::String(desc)) => Some(desc.clone()), ports.insert(get_port_number(v)?, PortConfig{enabled:true, description:None});
Some(v) => bail!("expect a string description, got {v:?}"), }
None => None, ports
}; }),
Ok(PortConfig { enabled, description }) Some(v) => bail!("ports must be either a table of '<port> = ...' or an array of ports, got {:?}", v),
},
_ => bail!("expected either a boolean (enabled), a string (description), or a table for a port config, got {value:?}"),
} }
} }
@ -193,258 +174,3 @@ fn get_port_number(v: &Value) -> Result<u16> {
}; };
Ok(port) Ok(port)
} }
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
fn config_test(config: &str, expected: Config) {
let config = config.parse::<Value>().expect("case not toml");
let config = parse_config(&config).expect("unable to parse config");
assert_eq!(expected, config);
}
fn config_error_test(config: &str) {
let config = config.parse::<Value>().expect("case not toml");
assert!(parse_config(&config).is_err());
}
#[test]
fn empty() {
config_test("", Config { auto: true, servers: HashMap::new() });
}
#[test]
fn auto_false() {
config_test(
"
auto=false
",
Config { auto: false, servers: HashMap::new() },
);
}
#[test]
fn auto_not_boolean() {
config_error_test(
"
auto='what is going on'
",
);
}
#[test]
fn servers_not_table() {
config_error_test("servers=1234");
}
#[test]
fn servers_default() {
config_test("servers.foo={}", {
let mut servers = HashMap::new();
servers.insert(
"foo".to_string(),
ServerConfig { auto: true, ports: HashMap::new() },
);
Config { auto: true, servers }
})
}
#[test]
fn servers_auto_false() {
config_test(
"
[servers.foo]
auto=false
",
{
let mut servers = HashMap::new();
servers.insert(
"foo".to_string(),
ServerConfig { auto: false, ports: HashMap::new() },
);
Config { auto: true, servers }
},
)
}
#[test]
fn servers_auto_not_bool() {
config_error_test(
"
[servers.foo]
auto=1234
",
)
}
#[test]
fn servers_ports_list() {
config_test(
"
[servers.foo]
ports=[1,2,3]
",
{
let mut servers = HashMap::new();
servers.insert(
"foo".to_string(),
ServerConfig {
auto: true,
ports: {
let mut ports = HashMap::new();
ports.insert(
1,
PortConfig { enabled: true, description: None },
);
ports.insert(
2,
PortConfig { enabled: true, description: None },
);
ports.insert(
3,
PortConfig { enabled: true, description: None },
);
ports
},
},
);
Config { auto: true, servers }
},
)
}
#[test]
fn servers_ports_table_variations() {
config_test(
"
[servers.foo.ports]
1=true
2={enabled=false}
3=false
",
{
let mut servers = HashMap::new();
servers.insert(
"foo".to_string(),
ServerConfig {
auto: true,
ports: {
let mut ports = HashMap::new();
ports.insert(
1,
PortConfig { enabled: true, description: None },
);
ports.insert(
2,
PortConfig {
enabled: false,
description: None,
},
);
ports.insert(
3,
PortConfig {
enabled: false,
description: None,
},
);
ports
},
},
);
Config { auto: true, servers }
},
)
}
#[test]
fn servers_ports_table_descriptions() {
config_test(
"
[servers.foo.ports]
1={enabled=false}
2={description='humble'}
",
{
let mut servers = HashMap::new();
servers.insert(
"foo".to_string(),
ServerConfig {
auto: true,
ports: {
let mut ports = HashMap::new();
ports.insert(
1,
PortConfig {
enabled: false,
description: None,
},
);
ports.insert(
2,
PortConfig {
enabled: true,
description: Some("humble".to_string()),
},
);
ports
},
},
);
Config { auto: true, servers }
},
)
}
#[test]
fn servers_ports_raw_desc() {
config_test(
"
[servers.foo.ports]
1='humble'
",
{
let mut servers = HashMap::new();
servers.insert(
"foo".to_string(),
ServerConfig {
auto: true,
ports: {
let mut ports = HashMap::new();
ports.insert(
1,
PortConfig {
enabled: true,
description: Some("humble".to_string()),
},
);
ports
},
},
);
Config { auto: true, servers }
},
)
}
#[test]
fn servers_inherit_auto() {
config_test(
"
auto=false
servers.foo={}
",
{
let mut servers = HashMap::new();
servers.insert(
"foo".to_string(),
ServerConfig { auto: false, ports: HashMap::new() },
);
Config { auto: false, servers }
},
)
}
}