Compare commits

..

3 commits

Author SHA1 Message Date
Brandon W Maister
05c0a0a4de make server logging show messages in the frontend
now you can use the log crate and get messages in the frontend.
2024-07-31 15:35:11 -07:00
Brandon W Maister
6c10d8eece feat: Discover docker ports as well
If processes are running in a container then the fwd process
can't read their internal FDs without the CAP_SYS_ADMIN property
which is equivalent to sudo. Even with sudo, I think you need to do
a lot of work to be able to read them -- spawning a process within
the cgroup, doing work there, and then communicating back.

This just uses the docker api to populate some default ports, which
later get overwritten if fwd can find a native process.

The Docker port scan takes about 1.5ms, and the full port scan takes
40+ms, so this adds basically no overhead.
2024-07-31 15:32:45 -07:00
Brandon W Maister
66da323481 feat: Show errored ports as an error state 2024-07-31 15:32:04 -07:00
154 changed files with 1160 additions and 5114 deletions

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose

View file

@ -1,3 +1,10 @@
# From https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml
# Which is also via https://eugene-babichenko.github.io/blog/2020/05/09/github-actions-cross-platform-auto-releases/
# ...both of which are very good.
#
# I'm sure I don't need half the stuff I have in here (around cargo
# customization and whatnot) but.
#
name: release
on:
@ -6,96 +13,116 @@ on:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
jobs:
create_release:
name: Create release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get the release version
if: env.VERSION == ''
run: echo "VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
- name: Show the version
run: |
echo "version is: $VERSION"
- name: Create GitHub release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release create $VERSION --draft --verify-tag --title $VERSION
runs-on: ubuntu-22.04
outputs:
version: ${{ env.VERSION }}
upload_url: ${{ steps.create_release.outputs.upload_url }}
build_release:
name: Build all the stuff
steps:
- name: Create GitHub release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: true
release_assets:
name: Release assets
needs: ['create_release'] # We need to know the upload URL
runs-on: ${{ matrix.os }} # We run many different builds
env:
# For some builds, we use cross to test on 32-bit and big-endian
# systems.
CARGO: cargo
# When CARGO is set to CROSS, this is set to `--target matrix.target`.
TARGET_FLAGS: ""
# When CARGO is set to CROSS, TARGET_DIR includes matrix.target.
TARGET_DIR: ./target
# Emit backtraces on panics.
RUST_BACKTRACE: 1
# Build static releases with PCRE2.
PCRE2_SYS_STATIC: 1
strategy:
fail-fast: false
matrix:
build: ['linux', 'debian', 'macos', 'arm-macos', 'windows']
build: ['linux', 'macos', 'arm-macos', 'windows']
include:
- build: linux
os: ubuntu-22.04
rust: nightly
target: x86_64-unknown-linux-musl
packages: apt
- build: debian
os: ubuntu-22.04
target: x86_64-unknown-linux-musl
packages: apt
- build: macos
os: macos-latest
os: macos-12
rust: nightly
target: x86_64-apple-darwin
packages: brew
- build: arm-macos
os: macos-latest
os: macos-12
rust: nightly
target: aarch64-apple-darwin
packages: brew
- build: windows
os: windows-2022
rust: nightly
target: x86_64-pc-windows-msvc
packages: none
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install packages (linux)
if: matrix.packages == 'apt'
run: |
sudo apt-get update
sudo apt-get install -y pandoc
- name: Install packages (macos)
if: matrix.packages == 'brew'
run: |
brew update
brew install pandoc
- name: Install rust
uses: dtolnay/rust-toolchain@stable
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
- name: Run the release automation
- name: Use Cross
shell: bash
run: |
cargo install cross
echo "CARGO=cross" >> $GITHUB_ENV
echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV
echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV
- name: Build release binary
run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }}
- name: Strip release binary (linux and macos)
if: matrix.build == 'linux' || matrix.build == 'macos' || matrix.build == 'arm-macos'
run: |
strip "target/${{ matrix.target }}/release/fwd"
strip "target/${{ matrix.target }}/release/fwd-browse"
- name: Build archive
shell: bash
run: |
staging="fwd-${{ matrix.target }}"
mkdir -p "$staging"
if [ "${{ matrix.os }}" = "windows-2022" ]; then
cp "target/${{ matrix.target }}/release/fwd.exe" "$staging/"
7z a "$staging.zip" "$staging"
echo "ASSET=$staging.zip" >> $GITHUB_ENV
else
cp "target/${{ matrix.target }}/release/fwd" "$staging/"
cp "target/${{ matrix.target }}/release/fwd-browse" "$staging/"
tar czf "$staging.tar.gz" "$staging"
echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV
fi
- name: Upload release archive
uses: actions/upload-release-asset@v1.0.2
env:
RELEASE_TAG: ${{ needs.create_release.outputs.version }}
BUILD: ${{ matrix.build }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 release.py
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_name: ${{ env.ASSET }}
asset_path: ${{ env.ASSET }}
asset_content_type: application/octet-stream

1092
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,12 @@
[package]
name = "fwd"
version = "0.9.2"
authors = ["John Doty <john@d0ty.me>"]
version = "0.9.0"
edition = "2021"
license = "MIT"
description = "Automatically forward ports to a remote server"
readme = "README.md"
documentation = "https://github.com/DeCarabas/fwd"
description = "Automatically forward ports to a remote server over ssh"
homepage = "https://github.com/DeCarabas/fwd"
repository = "https://github.com/DeCarabas/fwd"
readme = "README.md"
[[bin]]
name = "fwd-browse"
@ -17,48 +15,29 @@ bench = false
[dependencies]
anyhow = "1.0"
bollard = "0.17.0"
bytes = "1"
copypasta = "0.10.1"
crossterm = { version = "0.28.1", features = ["event-stream"] }
directories-next = "2"
crossterm = { version = "0.25", features = ["event-stream"] }
env_logger = { version = "0.11.5", default-features = false }
home = "0.5.4"
indoc = "1"
log = { version = "0.4", features = ["std"] }
open = "3"
rand = "0.8.5"
ratatui = "0.28.0"
thiserror = "1.0"
tokio = { version = "1", features = ["io-std", "io-util", "macros", "net", "process", "rt", "rt-multi-thread", "fs"] }
tokio-stream = "0.1"
toml = "0.5"
tui = "0.19"
xdg = "2"
[dev-dependencies]
assert_matches = "1"
pretty_assertions = "1"
tempfile = "3"
tempdir = "0.3"
[target.'cfg(target_os="linux")'.dependencies]
procfs = "0.14.1"
[target.'cfg(target_family="unix")'.dependencies]
libc = "0.2.155"
[package.metadata.deb]
section = "net"
depends = [] # No auto deps?
assets = [
["target/release/fwd", "usr/bin/", "755"],
["target/release/fwd-browse", "usr/bin/", "755"],
["LICENSE", "usr/share/doc/fwd/", "644"],
["README.md", "usr/share/doc/fwd/README", "644"],
# The man page is automatically generated by fwd's build process. See
# release.py for details.
["target/release/fwd.1", "usr/share/man/man1/fwd.1", "644"],
]
extended-description = """\
fwd enumerates the listening ports the remote server and automatically listens
for connections on the same ports on the local machine. When fwd receives a
connection on the local machine, it automatically forwards that connection to
the remote machine.
"""
users = "0.11"

View file

@ -1,4 +1,4 @@
Copyright 2024 John Doty
Copyright 2022 John Doty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -4,7 +4,7 @@ A port-forwarding utility.
Here's how it works:
1. Get the latest [release](https://github.com/DeCarabas/fwd/releases) of `fwd`
2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`, or `.local/bin`)
2. You install `fwd` on the server somewhere in your `$PATH` (like `/usr/bin/`)
3. You install `fwd` on the client (like your laptop)
4. You run `fwd` on the client to connect to the server, like so:
@ -13,9 +13,6 @@ doty@my.laptop$ fwd some.server
```
`fwd` will connect to `some.server` via ssh, and then show you a screen listing all of the ports that the server is listening on locally.
<img width="1337" src="doc/screenshot-01.png" alt="A terminal displaying a list of ports and descriptions. Some are dimmed and one is highlighted." />
Use the up and down arrow keys (or `j`/`k`) to select the port you're interested in and press `e` to toggle forwarding of that port.
Now, connections to that port locally will be forwarded to the remote server.
@ -24,3 +21,9 @@ If the port is something that might be interesting to a web browser, you can pre
If something is going wrong, pressing `l` will toggle logs that might explain it.
Press `q` to quit.
## Future Improvements:
- Clipboard integration: send something from the remote end of the pipe to the host's clipboard. (Sometimes you *really* want to copy some big buffer from the remote side and your terminal just can't make that work.)
- Client heartbeats: I frequently wind up in a situation where the pipe is stalled: not broken but nothing is getting through. (This happens with my coder.com pipes all the time.)

148
build.rs
View file

@ -1,148 +0,0 @@
use std::io::Write;
use std::path::{absolute, Path, PathBuf};
/// Fetch the contents of the given file, and also tell cargo that we looked
/// in there.
fn file_contents<P: AsRef<Path>>(path: P) -> String {
let path =
absolute(path.as_ref()).expect("Unable to make the path absolute");
let mut stdout = std::io::stdout();
stdout
.write_all(b"cargo::rerun-if-changed=")
.expect("Unable to write stdout");
stdout
.write_all(path.as_os_str().as_encoded_bytes())
.expect("Unable to write path to stdout");
stdout
.write_all(b"\n")
.expect("Unable to write newline to stdout");
std::fs::read_to_string(path).expect("Unable to read file")
}
fn git_rel<P: AsRef<Path>>(path: P) -> PathBuf {
let output = std::process::Command::new("git")
.arg("rev-parse")
.arg("--show-toplevel")
.output()
.expect("Error launching git rev-parse");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf8");
eprintln!("`git rev-parse --show-toplevel` failed, stderr: {stderr}");
panic!("`git rev-parse --show-toplevel` failed");
}
let mut root = PathBuf::from(
std::str::from_utf8(&output.stdout)
.expect("Output was not utf-8")
.trim(),
);
root.push(path);
root
}
/// Emit the current git commit.
fn emit_git_commit() {
// Fetch the current commit from the head. We do it this way instead of
// asking `git rev-parse` to do it for us because we want to reliably
// tell cargo which files it should monitor for changes.
let head = file_contents(git_rel(".git/HEAD"));
let rev = if let Some(r) = head.strip_prefix("ref: ") {
let mut ref_path = git_rel(".git/");
ref_path.push(r.trim());
file_contents(ref_path)
} else {
head
};
// But *now* we ask git rev-parse to make this into a short hash (a) to
// make sure we got it right and (b) because git knows how to quickly
// determine how much of a commit is required to be unique. We don't need
// to tell cargo anything here, no file that git consults will be
// mutable.
let output = std::process::Command::new("git")
.arg("rev-parse")
.arg("--short")
.arg(rev.trim())
.output()
.expect("could not spawn `git` to get the hash");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf8");
eprintln!("`git rev-parse --short HEAD` failed, stderr: {stderr}");
panic!("`git rev-parse --short HEAD` failed");
}
let rev = std::str::from_utf8(&output.stdout)
.expect("git did not output utf8")
.trim();
println!("cargo::rustc-env=REPO_REV={rev}");
}
fn emit_git_dirty() {
// Here is the way to see if anything is up with the repository: run `git
// status --porcelain=v1`. The status output in the v1 porcelain format
// has one line for every file that's modified in some way: staged,
// changed but unstaged, untracked, you name it. Files in the working
// tree that are up to date with the repository are not emitted. This is
// exactly what we want.
//
// (Yes, I want to track untracked files, because they can mess with the
// build too. The only good build is a clean build!)
let output = std::process::Command::new("git")
.arg("status")
.arg("-z")
.arg("--porcelain=v1")
.output()
.expect("could not spawn `git` to get repository status");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf8");
eprintln!("`git status` failed, stderr: {stderr}");
panic!("`git status` failed");
}
let output =
std::str::from_utf8(&output.stdout).expect("git did not output utf8");
// Emit the repository status.
let dirty = if output.trim().is_empty() {
""
} else {
" *dirty*"
};
println!("cargo::rustc-env=REPO_DIRTY={dirty}");
// NOW: The output here has to do with *all* of the files in the git
// respository. (Because if nothing was modified, but then *becomes*
// modified, we need to rerun the script to notice the dirty bit.)
// `git-ls-files` is the way to do that.
let output = std::process::Command::new("git")
.arg("ls-files")
.arg("-z")
.arg("--cached")
.arg("--deleted")
.arg("--modified")
.arg("--others")
.arg("--exclude-standard")
.output()
.expect("could not spawn `git` to get repository status");
if !output.status.success() {
let stderr = std::str::from_utf8(&output.stderr)
.expect("git failed and stderr was not utf-8");
eprintln!("`git ls-files` failed, stderr: {stderr}");
panic!("`git ls-files` failed");
}
let output =
std::str::from_utf8(&output.stdout).expect("git did not output utf8");
for fname in output.split_terminator("\0") {
println!("cargo::rerun-if-changed={fname}");
}
}
fn main() {
emit_git_commit();
emit_git_dirty();
}

View file

@ -1,156 +0,0 @@
% fwd(1)
% John Doty <john@d0ty.me>
% August 2024
# NAME
fwd - Automatically forward connections to remote machines
# SYNOPSIS
**fwd** [OPTIONS] SERVER
**fwd** [OPTIONS] browse URL
**fwd** [OPTIONS] clip FILE
**fwd-browse** URL
# DESCRIPTION
**fwd** enumerates the listening ports the remote server and automatically listens for connections on the same ports on the local machine.
When **fwd** receives a connection on the local machine, it automatically forwards that connection to the remote machine.
**-s**, **-\-sudo**
: Run the server side of fwd with `sudo`.
: This allows the client to forward ports that are open by processes being run under other accounts (e.g., docker containers being run as root), but requires sudo access on the server and *might* end up forwarding ports that you do not want forwarded (e.g., port 22 for sshd, or port 53 for systemd.)
**-\-log-filter** **FILTER**
: Set remote server's log level. Default is `warn`.
: Supports all of Rust's env_logger filter syntax, e.g. `--log-filter=fwd::trace`.
**-\-version**
: Print the version of fwd and exit.
# INTERACTIVE COMMANDS
Once **fwd** is connected, it displays an interactive list of the ports available on the remote server.
- Ports that **fwd** is listening on are displayed in the default terminal color.
- Ports that **fwd** is aware of but which are disabled are displayed in dark gray.
- Ports that **fwd** has tried to listen on but which have failed are displayed in red.
Details on the error may be found in the log window.
Disabling and re-enabling the port will cause **fwd** to try again.
The following commands are available while **fwd** is connected:
**Esc, q, Ctrl-C**
: Exit **fwd**.
**?, h**
: Display the help window.
**Up, k**
: Select the previous port in the list.
**Down, j**
: Select the next port in the list.
**Enter**
: Attempt to browse to localhost on the specified port with the default browser.
**a**
: Hide or show anonymous ports.
: (See "identifying ports" below for more information on anonymous ports.)
**e**
: Enable or disable the selected port.
**l**
: Show or hide the log window.
# IDENTIFYING PORTS
**fwd** enumerates all of the ports that the remote server is listening on, and attempts to identify the process that is listening on each port.
It can identify ports in the following ways:
*docker*
: **fwd** will attempt to find and connect to a docker engine on the remote machine.
: If successful, it will list all of the forwarded ports, and identify each port as belonging to that docker container.
*procfs*
: On Linux, the listening ports are found by reading procfs and mapping them back to process command lines.
: **fwd** can only identify processes that the user it is connected as has permissions to read on the remote machine.
(Earlier methods take precedence over later methods.)
If **fwd** cannot identify the process that is listening on a given port, then the port is *anonymous*.
Anonymous ports are not enabled by default, but can be enabled manually, either with the UI or by configuration.
# OPENING BROWSERS
**fwd** can be used to open URLs in the default browser on the local machine.
Run **fwd browse URL** on the remote server to open the `URL` in the default browser on the local machine.
This only works if **fwd** is connected, and if the user running **fwd browse** is the same as the user that connected the **fwd** session.
The **fwd-browse** program acts as a wrapper around **fwd browse**, to be used with configurations that can't handle a browser being a program with an argument.
# CLIPBOARD
**fwd** can be used from the remote machine to place text on the clipboard of the local machine.
Run **fwd clip FILE** to copy the contents of the named file to the clipboard.
If **FILE** is **-**, this reads text from stdin instead.
# CONFIGURATION
**fwd** can be configured with a configuration file.
- On Windows, the config file will be in your roaming AppData folder.
(e.g., *c:\\Users\\Winifred\\AppData\\Roaming\\fwd\\config\\config.toml*)
- On MacOS, the config file will be in *$HOME/Library/Application Support/fwd/config.toml*.
(e.g., /Users/Margarie/Library/Application Support/fwd/config.toml)
- On XDG-ish systems (like Linux), the config file is in *~/.config/fwd/config.toml*.
(e.g., */home/lynette/.config/fwd/config.toml*)
The following is an example of a *config.toml* file:
```
auto=true # should `fwd` should enable identified ports (default true)
[servers.foo] # Server-specific settings for foo
auto=true # defaults to the global setting
ports=[1080, 1082] # ports that are always present
[servers.bar.ports] # `ports` can also be a table with port numbers as keys
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
enabled=true
description="A humble python"
```
Ports that are specified in the configuration file will always be present in the list of ports for a given server, even if no process is listening on that port.
# TROUBLESHOOTING
Connections are made via the **ssh** command.
Your **ssh** must:
- Be on your path, so that **fwd** can find it to invoke it
- Be able to authenticate you to the remote server.
(Interactive authentication is fine.)
- Understand the **-D** command line option, to operate as a SOCKS5 server
- Be able to start the **fwd** command on the remote server
A typical ssh invocation from **fwd** looks like:
```bash
ssh -T -D XXXX me@server FWD_LOG=warning FWD_SEND_ANONYMOUS=1 fwd --server
```
**fwd** only enumerates ports that are listening on loopback addresses (e.g., 127.0.0.1) or on all addresses (e.g., 0.0.0.0).
If it cannot find a particular port, check to make sure that the process listening on that port is accessible via localhost.
# SEE ALSO
ssh(1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

4
fuzz/.gitignore vendored
View file

@ -1,4 +0,0 @@
target
corpus
artifacts
coverage

1735
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,30 +0,0 @@
[package]
name = "fwd-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
arbitrary = { version = "1.3.2", features = ["derive"] }
libfuzzer-sys = "0.4"
serde_json = "1.0.124"
[dependencies.fwd]
path = ".."
[[bin]]
name = "json_raw_input"
path = "fuzz_targets/json_raw_input.rs"
test = false
doc = false
bench = false
[[bin]]
name = "json_only_valid_serde"
path = "fuzz_targets/json_only_valid_serde.rs"
test = false
doc = false
bench = false

View file

@ -1,77 +0,0 @@
#![no_main]
use arbitrary::{Arbitrary, Error, Unstructured};
use libfuzzer_sys::fuzz_target;
use std::collections::HashMap;
extern crate fwd;
use fwd::server::refresh::docker::JsonValue;
/// InputNumber is a JSON number, i.e., a finite 64-bit floating point value
/// that is not NaN. We need to define our own little wrapper here so that we
/// can convince Arbitrary to only make finite f64s.
///
/// Ideally we would actually wrap serde_json::Number but there are rules
/// about mixing 3rd party traits with 3rd party types.
#[derive(Debug, PartialEq)]
struct InputNumber(f64);
impl<'a> Arbitrary<'a> for InputNumber {
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self, Error> {
let value = f64::arbitrary(u)?;
if value.is_finite() {
Ok(InputNumber(value))
} else {
Err(Error::IncorrectFormat) // REJECT
}
}
#[inline]
fn size_hint(depth: usize) -> (usize, Option<usize>) {
f64::size_hint(depth)
}
}
/// TestInput is basically serde_json::Value, except (a) it has a HashMap and
/// not serde_json's special `Map` structure, and (b) it has `InputNumber`
/// instead of `json_serde::Number` for reasons described above.
#[derive(Debug, PartialEq, Arbitrary)]
enum TestInput {
Null,
Bool(bool),
Number(InputNumber),
String(String),
Object(HashMap<String, TestInput>),
Array(Vec<TestInput>),
}
fn convert(value: &TestInput) -> serde_json::Value {
match value {
TestInput::Null => serde_json::Value::Null,
TestInput::Bool(b) => serde_json::Value::Bool(*b),
TestInput::Number(n) => serde_json::Value::Number(
serde_json::Number::from_f64(n.0).expect("Unable to make an f64"),
),
TestInput::String(s) => serde_json::Value::String(s.clone()),
TestInput::Object(o) => {
let mut out = serde_json::map::Map::new();
for (k, v) in o.into_iter() {
out.insert(k.clone(), convert(v));
}
serde_json::Value::Object(out)
}
TestInput::Array(v) => {
serde_json::Value::Array(v.into_iter().map(convert).collect())
}
}
}
fuzz_target!(|data: TestInput| {
// Convert the arbitrary TestInput into an arbitrary serde_json::Value,
// then use serde_json to write out arbitrary JSON.
let converted = convert(&data).to_string();
// Parse the JSON that serde_json produced. This fuzz test should ensure
// that we can parse anything that serde_json can produce.
let _ = JsonValue::parse(converted.as_bytes());
});

View file

@ -1,10 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
extern crate fwd;
use fwd::server::refresh::docker::JsonValue;
fuzz_target!(|data: &[u8]| {
let _ = JsonValue::parse(data);
});

View file

@ -1,161 +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 enum
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")
class Archive(enum.Enum):
TARBALL = 1
ZIP = 2
DEB = 3
@dataclasses.dataclass
class BuildSettings:
target: str # The rust target to build for
test: bool = True # Whether or not to run tests
man_page: bool = True # Whether or not to generate a man page
strip: bool = True # Whether or not to strip binaries
archive: Archive = Archive.TARBALL # Archive type
ext: str = "" # The file extension of the binary
print(f"doing release: {BUILD}")
build = {
"linux": BuildSettings(
target="x86_64-unknown-linux-musl",
),
"debian": BuildSettings(
target="x86_64-unknown-linux-musl",
test=False,
archive=Archive.DEB,
),
"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,
archive=Archive.ZIP,
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)
def build_archive(staging: pathlib.Path) -> pathlib.Path:
print("Creating archive...")
if build.archive == Archive.ZIP:
archive = pathlib.Path(f"{staging}.zip")
subprocess.run(["7z", "a", archive, f"{staging}"], check=True)
elif build.archive == Archive.DEB:
subprocess.run(["cargo", "install", "cargo-deb"], check=True)
shutil.copyfile(staging / "fwd.1", target_dir / "fwd.1")
subprocess.run(["cargo", "deb", "--target", build.target], check=True)
# Knowing the deb path means knowing the target version but I don't
# actually have the version here. (Or, like, I have the release tag
# but not in testing.) So just find the hopefully singular .deb that
# we made.
deb_path = pathlib.Path("target") / build.target / "debian"
archives = list(deb_path.glob("*.deb"))
assert len(archives) == 1
archive = archives[0]
else:
assert build.archive == Archive.TARBALL
archive = pathlib.Path(f"{staging}.tar.gz")
subprocess.run(["tar", "czf", archive, f"{staging}"], check=True)
return archive
staging = pathlib.Path(f"fwd-{build.target}")
os.makedirs(staging, exist_ok=True)
build_and_test(staging)
build_docs(staging)
archive = build_archive(staging)
shutil.rmtree(staging)
assert archive.exists()
if RELEASE_TAG is None:
print(f"Not releasing {archive} 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,14 +0,0 @@
# Test JSON
This directory contains test JSON files from https://github.com/nst/JSONTestSuite as of commit 984defc.
It only has the positive and questionable JSON inputs, as our JSON parser is extremely forgiving, by design.
## Filtered tests
Some of the questionable tests have been removed:
- `i_structure_UTF-8_BOM_empty_object.json` removed because we don't handle BOMs.
- `i_string_utf16LE_no_BOM.json` removed because we don't speak UTF16.
- `i_string_utf16BE_no_BOM.json` removed because we don't speak UTF16.
- `i_string_UTF-16LE_with_BOM.json` removed because we don't speak UTF16.

View file

@ -1 +0,0 @@
[123.456e-789]

View file

@ -1 +0,0 @@
[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006]

View file

@ -1 +0,0 @@
[-1e+9999]

View file

@ -1 +0,0 @@
[1.5e+9999]

View file

@ -1 +0,0 @@
[-123123e100000]

View file

@ -1 +0,0 @@
[123123e100000]

View file

@ -1 +0,0 @@
[123e-10000000]

View file

@ -1 +0,0 @@
[-123123123123123123123123123123]

View file

@ -1 +0,0 @@
[100000000000000000000]

View file

@ -1 +0,0 @@
[-237462374673276894279832749832423479823246327846]

View file

@ -1 +0,0 @@
{"\uDFAA":0}

View file

@ -1 +0,0 @@
["\uDADA"]

View file

@ -1 +0,0 @@
["\uD888\u1234"]

View file

@ -1 +0,0 @@
["譌・ム淫"]

View file

@ -1 +0,0 @@
["<22><><EFBFBD>"]

View file

@ -1 +0,0 @@
["\uDd1ea"]

View file

@ -1 +0,0 @@
["\uD800\uD800\n"]

View file

@ -1 +0,0 @@
["\ud800"]

View file

@ -1 +0,0 @@
["\ud800abc"]

View file

@ -1 +0,0 @@
["<22>"]

View file

@ -1 +0,0 @@
["\uDd1e\uD834"]

View file

@ -1 +0,0 @@
["И"]

View file

@ -1 +0,0 @@
["\uDFAA"]

View file

@ -1 +0,0 @@
["<22><><EFBFBD><EFBFBD>"]

View file

@ -1 +0,0 @@
["<22><>"]

View file

@ -1 +0,0 @@
["<22>ソソソソ"]

View file

@ -1 +0,0 @@
["<22>€€€€"]

View file

@ -1 +0,0 @@
["<22><>"]

View file

@ -1 +0,0 @@
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]

View file

@ -1 +0,0 @@
[[] ]

View file

@ -1 +0,0 @@
[""]

View file

@ -1 +0,0 @@
[]

View file

@ -1 +0,0 @@
["a"]

View file

@ -1 +0,0 @@
[false]

View file

@ -1 +0,0 @@
[null, 1, "1", {}]

View file

@ -1 +0,0 @@
[null]

View file

@ -1,2 +0,0 @@
[1
]

View file

@ -1 +0,0 @@
[1]

View file

@ -1 +0,0 @@
[1,null,null,null,2]

View file

@ -1 +0,0 @@
[2]

View file

@ -1 +0,0 @@
[123e65]

View file

@ -1 +0,0 @@
[0e+1]

View file

@ -1 +0,0 @@
[0e1]

View file

@ -1 +0,0 @@
[ 4]

View file

@ -1 +0,0 @@
[-0.000000000000000000000000000000000000000000000000000000000000000000000000000001]

View file

@ -1 +0,0 @@
[20e1]

View file

@ -1 +0,0 @@
[-0]

View file

@ -1 +0,0 @@
[-123]

View file

@ -1 +0,0 @@
[-1]

View file

@ -1 +0,0 @@
[-0]

View file

@ -1 +0,0 @@
[1E22]

View file

@ -1 +0,0 @@
[1E-2]

View file

@ -1 +0,0 @@
[1E+2]

View file

@ -1 +0,0 @@
[123e45]

View file

@ -1 +0,0 @@
[123.456e78]

View file

@ -1 +0,0 @@
[1e-2]

View file

@ -1 +0,0 @@
[1e+2]

View file

@ -1 +0,0 @@
[123]

View file

@ -1 +0,0 @@
[123.456789]

View file

@ -1 +0,0 @@
{"asd":"sdf", "dfg":"fgh"}

View file

@ -1 +0,0 @@
{"asd":"sdf"}

View file

@ -1 +0,0 @@
{"a":"b","a":"c"}

View file

@ -1 +0,0 @@
{"a":"b","a":"b"}

View file

@ -1 +0,0 @@
{}

View file

@ -1 +0,0 @@
{"":0}

View file

@ -1 +0,0 @@
{"foo\u0000bar": 42}

View file

@ -1 +0,0 @@
{ "min": -1.0e+28, "max": 1.0e+28 }

View file

@ -1 +0,0 @@
{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}

View file

@ -1 +0,0 @@
{"a":[]}

View file

@ -1 +0,0 @@
{"title":"\u041f\u043e\u043b\u0442\u043e\u0440\u0430 \u0417\u0435\u043c\u043b\u0435\u043a\u043e\u043f\u0430" }

View file

@ -1,3 +0,0 @@
{
"a": "b"
}

View file

@ -1 +0,0 @@
["\u0060\u012a\u12AB"]

View file

@ -1 +0,0 @@
["\uD801\udc37"]

View file

@ -1 +0,0 @@
["\ud83d\ude39\ud83d\udc8d"]

View file

@ -1 +0,0 @@
["\"\\\/\b\f\n\r\t"]

View file

@ -1 +0,0 @@
["\\u0000"]

View file

@ -1 +0,0 @@
["\""]

View file

@ -1 +0,0 @@
["a/*b*/c/*d//e"]

View file

@ -1 +0,0 @@
["\\a"]

View file

@ -1 +0,0 @@
["\\n"]

View file

@ -1 +0,0 @@
["\u0012"]

View file

@ -1 +0,0 @@
["\uFFFF"]

Some files were not shown because too many files have changed in this diff Show more