2
0
mirror of https://github.com/xvxx/phd synced 2024-11-16 12:13:06 +00:00

Compare commits

...

83 Commits

Author SHA1 Message Date
chris west
5b02ccc1db chore: Release phd version 0.1.15 2022-12-04 14:56:11 -08:00
chris west
1e31a05f9d update cargo-release 2022-12-04 14:52:55 -08:00
chris west
006a3f58a5 update changelog 2022-12-04 14:52:04 -08:00
chris west
6761e1d738 update alphanumeric-sort dependency 2022-11-27 18:41:21 -08:00
chris west
ae866b4609 update cargo install instructions 2022-11-27 18:18:26 -08:00
chris west
555504689f update gh actions 2022-11-27 18:16:35 -08:00
chris west
a0ab2fa9c5
update status 2022-01-05 10:45:11 -08:00
chris west
fb304bce9e (cargo-release) start next development iteration 0.1.15-dev 2020-11-16 19:49:40 -08:00
chris west
be85890d84 (cargo-release) version 0.1.14 2020-11-16 19:49:19 -08:00
chris west
2e78294135 NO_COLOR support 2020-11-16 19:49:01 -08:00
chris west
0db0c7e149 update changelog on release 2020-11-16 19:48:50 -08:00
chris west
10ba1ca69d (cargo-release) start next development iteration 0.1.14-dev 2020-08-22 12:33:55 -07:00
chris west
fe7e2e43e6 (cargo-release) version 0.1.13 2020-08-22 12:33:46 -07:00
chris west
4c0d707a7a update changelog 2020-08-22 12:30:37 -07:00
chris west
06c0a2d5ea also look for ? to start query string
fixes #3
2020-08-22 12:23:40 -07:00
chris west
954855fd80 update changelog 2020-08-07 18:55:45 -07:00
chris west
1cb71b3450 If -p is passed without --bind, bind to that port 2020-08-07 18:37:45 -07:00
chris west
4de39201ba how'd you get down there 2020-08-07 18:33:04 -07:00
chris west
4ee7008070 --no-color to hide colors in log messages 2020-08-07 18:32:56 -07:00
chris west
de8607bee8 (cargo-release) start next development iteration 0.1.13-dev 2020-06-27 10:10:40 -07:00
chris west
db0de166e8 (cargo-release) version 0.1.12 2020-06-27 10:10:35 -07:00
chris west
19e7579a4f update changelog for 0.1.12 2020-06-27 10:10:12 -07:00
chris west
f3ff3fb3eb mention -b in readme 2020-06-27 10:04:36 -07:00
chris west
924c5b1ae9 clarify 2020-06-27 10:00:23 -07:00
chris west
68979f2d88 add --bind to -h 2020-06-27 09:59:19 -07:00
Richard Bradfield
a07c921fee
Default to dual stack and add BIND arg
Change the default bind address to be `[::]` and allow the user to
specify the socket to bind on.

This is distinct from the HOST/PORT pair already configurable as phd
might be running behind a proxy where links need to be generated with a
different destination to the bound socket.
2020-06-27 15:19:05 +01:00
chris west
f3bfbbd9f5 (cargo-release) start next development iteration 0.1.12-dev 2020-05-09 13:26:45 -07:00
chris west
3d19ac030c (cargo-release) version 0.1.11 2020-05-09 13:26:41 -07:00
chris west
e7e0ff9294 update changelog 2020-05-09 13:26:13 -07:00
chris west
dc1f48e917 basic manpage 2020-05-09 13:22:02 -07:00
chris west
3b0d9de144 (cargo-release) start next development iteration 0.1.11-dev 2020-05-09 13:05:36 -07:00
chris west
d63f0e866b (cargo-release) version 0.1.10 2020-05-09 13:05:31 -07:00
chris west
85865968e3 hide info msgs in -r mode 2020-05-09 13:04:51 -07:00
chris west
c0e0aa4548 add changelog 2020-05-09 12:55:45 -07:00
chris west
36cdefd32c use phetch's gopher Type enum 2020-05-09 12:51:18 -07:00
chris west
03b557312c bolder 2020-05-09 12:41:12 -07:00
chris west
822e1e0f26 -r for serverless rendering 2020-05-09 12:40:25 -07:00
chris west
b527304e67 different indicator 2020-05-09 11:31:26 -07:00
chris west
ee2d324a98 line 2020-05-09 00:15:46 -07:00
chris west
73a0016893 update badge url 2020-05-09 00:15:22 -07:00
chris west
63b12145ad badges 2020-05-09 00:14:42 -07:00
chris west
4d712e9a05 include fixture dir 2020-05-09 00:12:53 -07:00
chris west
497185c2e3 gh actions 2020-05-09 00:09:00 -07:00
chris west
4717780bfb (cargo-release) start next development iteration 0.1.10-dev 2020-05-09 00:03:59 -07:00
chris west
ab890ec450 (cargo-release) version 0.1.9 2020-05-09 00:03:54 -07:00
chris west
bc30c3a209 center logo 2020-05-09 00:03:08 -07:00
chris west
2714841316 format tweaks 2020-05-08 23:59:15 -07:00
chris west
49ae7abb63 always prefix lines without \t with i 2020-05-08 23:59:10 -07:00
chris west
67665c3327 avoid double // 2020-01-18 23:11:13 -08:00
chris west
9964d2c530 (cargo-release) start next development iteration 0.1.9-dev 2020-01-16 20:24:12 -08:00
chris west
c94b6c986f (cargo-release) version 0.1.8 2020-01-16 20:24:02 -08:00
chris west
a1eb708980 force docs, other warnings 2020-01-15 12:50:31 -08:00
chris west
1a102fa9a8 clippy 2020-01-15 12:44:09 -08:00
chris west
aeaf8d36c4 install with homebrew 2020-01-15 12:37:41 -08:00
chris west
08e3056f55 comments are ignored 2020-01-15 00:30:39 -08:00
chris west
1dd632e433 (cargo-release) start next development iteration 0.1.8-dev 2020-01-13 23:39:25 -08:00
chris west
5dc22bf677 (cargo-release) version 0.1.7 2020-01-13 23:39:21 -08:00
chris west
ac41b7f191 typos 2020-01-13 23:24:30 -08:00
chris west
1c2b065ee7 fix port generation 2020-01-13 23:16:04 -08:00
chris west
0241a698c8 let linker strip release binaries 2020-01-13 14:59:55 -08:00
chris west
3f6c5c43de geomyidae syntax works now 2020-01-13 14:26:32 -08:00
chris west
3f5fed5631 replace server/port with real values 2020-01-12 22:38:44 -08:00
chris west
82bbb4edd3 support geomyidae gophermap format 2020-01-12 22:23:30 -08:00
chris west
b90325579b test gophermap line parsing 2020-01-11 23:51:27 -08:00
chris west
5ae4663bf0 new url 2020-01-11 13:07:47 -08:00
dvkt
fc22f88363 upgrade 2020-01-08 16:14:05 -08:00
dvkt
5cc05f1349 need more tests 2020-01-08 16:11:28 -08:00
dvkt
87bc9a7a58 (cargo-release) start next development iteration 0.1.7-dev 2020-01-06 15:37:11 -08:00
dvkt
eeb342ebfd (cargo-release) version 0.1.6 2020-01-06 15:37:05 -08:00
dvkt
a1bb9c24d3 shorter 2020-01-06 15:36:11 -08:00
dvkt
525d06383a dirs always first 2020-01-06 15:35:05 -08:00
dvkt
d295650413 Merge branch 'master' of https://github.com/dvkt/phd 2020-01-06 15:30:17 -08:00
dvkt
01c327192d version sort directories 2020-01-06 15:30:14 -08:00
dvkt
f911b4a355 sort_paths (failing) test + fixtures 2020-01-06 15:13:10 -08:00
dvkt
8b825dbd5e Merge branch 'master' of https://github.com/dvkt/phd 2020-01-06 13:32:02 -08:00
dvkt
bdd127f665 gopher releases url 2020-01-06 13:31:54 -08:00
dvkt
4eebc84c1b phd header 2020-01-06 11:55:56 -08:00
dvkt
99178d51d9 indent 2020-01-06 11:37:07 -08:00
dvkt
49dab5b83d colors 2020-01-06 11:36:06 -08:00
dvkt
616a164baf ruby example 2020-01-06 11:35:19 -08:00
dvkt
43e596d01b fix version regex 2020-01-06 02:14:21 -08:00
dvkt
9ee7416403 add version badge to readme 2020-01-04 16:49:36 -08:00
dvkt
840c1dd5cc (cargo-release) start next development iteration 0.1.6-dev 2020-01-04 16:30:00 -08:00
33 changed files with 1379 additions and 186 deletions

2
.cargo/config Normal file
View File

@ -0,0 +1,2 @@
[build]
rustflags = ["-C", "link-arg=-s"]

73
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,73 @@
on:
push:
branches: [master]
pull_request:
branches: ["*"]
name: build
jobs:
test_macos:
name: Run Tests on macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Setup toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: clippy
override: true
- name: check
run: cargo check
- name: clippy
run: cargo clippy
- name: test
run: cargo test
- name: build
run: cargo build --release
test_ubuntu:
name: Run Tests on Ubuntu (x86_64)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: check
run: cargo check
- name: clippy
run: cargo clippy
- name: test
run: cargo test
- name: build
run: cargo build --release

162
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,162 @@
name: Create Release
on:
push:
tags:
- "v*.*.*"
jobs:
build_armv7:
name: Build for armv7
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: armv7-unknown-linux-gnueabihf
override: true
- name: Build release
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --locked --target armv7-unknown-linux-gnueabihf
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Package Binary
run: cp doc/phd.1 target/armv7-unknown-linux-gnueabihf/release && cd target/armv7-unknown-linux-gnueabihf/release && tar zcvf phd-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz phd phd.1
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: phd-linux-armv7
path: target/armv7-unknown-linux-gnueabihf/release/phd-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz
build_linux:
name: Build for Linux x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Build release
run: cargo build --locked --release
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Package Binary
run: cp doc/phd.1 target/release && cd target/release && tar zcvf phd-${{ steps.get_version.outputs.VERSION }}-linux-x86_64.tgz phd phd.1
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: phd-linux-x86_64
path: target/release/phd-${{ steps.get_version.outputs.VERSION }}-linux-x86_64.tgz
build_macos:
name: Build for macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Build release
uses: actions-rs/cargo@v1
with:
command: build
args: --locked --release
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Package Binary
run: cp doc/phd.1 target/release && cd target/release && zip -r phd-${{ steps.get_version.outputs.VERSION }}-macos.zip phd phd.1
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: phd-macos
path: target/release/phd-${{ steps.get_version.outputs.VERSION }}-macos.zip
create:
name: Create Release
needs: [build_armv7, build_linux, build_macos]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Download macOS artifact
uses: actions/download-artifact@v1
with:
name: phd-macos
- name: Download Linux (x86_64) artifact
uses: actions/download-artifact@v1
with:
name: phd-linux-x86_64
- name: Download Linux (armv7) artifact
uses: actions/download-artifact@v1
with:
name: phd-linux-armv7
- name: Create Release
uses: softprops/action-gh-release@v1
with:
draft: true
prerelease: true
files: |
phd-macos/phd-${{ steps.get_version.outputs.VERSION }}-macos.zip
phd-linux-x86_64/phd-${{ steps.get_version.outputs.VERSION }}-linux-x86_64.tgz
phd-linux-armv7/phd-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz
body_path: CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

62
CHANGELOG.md Normal file
View File

@ -0,0 +1,62 @@
## v0.1.15
- Update `alphanumeric-sort` dependency so `cargo install phd` works again.
- Recommend `cargo install phd --locked` in the README
## v0.1.14
- If the `NO_COLOR` env variable is set, colors won't be printed to
the log. Same as starting with `--no-color`.
See https://no-color.org/.
## v0.1.13
- Added `--no-color` command line option to not display color when
logging.
- Slight change to binding behavior: if `-p` is passed without `-b`,
we'll try to bind to that port. To this easier: `phd -p 7777`
- Accept `?` as query string indicator, not just `TAB`. See #3.
## v0.1.12
`phd` now uses `-b` and `--bind` to set the host and port to
bind to. `-p` and `-h` are now strictly for URL generation.
This should hopefully make it easier to run `phd` behind a
proxy and still generate proper links.
Thanks to @bradfier for the patch!
## v0.1.11
`phd` now ships with a basic manual!
It will be installed via homebrew and (eventually) AUR.
For now you can view it by cloning the repository and running:
man ./doc/phd.1
Enjoy!
## v0.1.10
`phd` can now render a single page to stdout, instead of starting
as a server. Those of us in the biz refer to this as "serverless".
For example, if your Gopher site lives in `/srv/gopher` and you want
to render the main page, just run:
phd -r / /srv/gopher
This will print the raw Gopher menu to stdout!
To view the "/about" page, pass that selector:
phd -r / /srv/gopher
Edge computing is now Gopher-scale! Enjoy!
## v0.1.9
Switch to using GitHub Actions for release automation.

53
Cargo.lock generated
View File

@ -1,67 +1,74 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]]
name = "alphanumeric-sort"
version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0"
[[package]] [[package]]
name = "content_inspector" name = "content_inspector"
version = "0.2.4" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
dependencies = [ dependencies = [
"memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "memchr",
] ]
[[package]]
name = "gophermap"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.5" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7"
dependencies = [ dependencies = [
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "libc",
] ]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.66" version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.2.1" version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.11.1" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
dependencies = [ dependencies = [
"hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "hermit-abi",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", "libc",
] ]
[[package]] [[package]]
name = "phd" name = "phd"
version = "0.1.5" version = "0.1.15"
dependencies = [ dependencies = [
"content_inspector 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "alphanumeric-sort",
"gophermap 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "content_inspector",
"threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "shell-escape",
"threadpool",
] ]
[[package]]
name = "shell-escape"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "170a13e64f2a51b77a45702ba77287f5c6829375b04a69cf2222acd17d0cfab9"
[[package]] [[package]]
name = "threadpool" name = "threadpool"
version = "1.7.1" version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
dependencies = [ dependencies = [
"num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus",
] ]
[metadata]
"checksum content_inspector 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
"checksum gophermap 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6ec2186bfad5a5dcbc9307dbc2d2444062300a836ae91b00dd80c3b71c34af3b"
"checksum hermit-abi 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f629dc602392d3ec14bfc8a09b5e644d7ffd725102b48b81e59f90f2633621d7"
"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
"checksum num_cpus 1.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76dac5ed2a876980778b8b85f75a71b6cbf0db0b1232ee12f826bccb00d09d72"
"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"

View File

@ -1,12 +1,12 @@
[package] [package]
name = "phd" name = "phd"
version = "0.1.5" version = "0.1.15"
authors = ["dvkt <c@dvkt.io>"] authors = ["chris west <c@xvxx.io>"]
license = "MIT" license = "MIT"
edition = "2018" edition = "2018"
description = "an esoteric gopher server" description = "an esoteric gopher server"
readme = "README.md" readme = "README.md"
repository = "https://github.com/dvkt/phd" repository = "https://github.com/xvxx/phd"
keywords = ["gopher", "server", "daemon"] keywords = ["gopher", "server", "daemon"]
exclude = [ exclude = [
"img/*" "img/*"
@ -18,15 +18,15 @@ codegen-units = 1
panic = 'abort' panic = 'abort'
opt-level = 'z' # Optimize for size. opt-level = 'z' # Optimize for size.
[package.metadata.release]
pre-release-replacements = [
{file="README.md", search="phd-v\\d+\\.\\d+\\.\\d+-", replace="{{crate_name}}-v{{version}}-"},
{file="README.md", search="/v\\d+\\.\\d+\\.\\d+/", replace="/v{{version}}/"},
{file="CHANGELOG.md", search="\\d+\\.\\d+\\.\\d+-dev", replace="{{version}}"},
]
[dependencies] [dependencies]
content_inspector = "0.2.4" content_inspector = "0.2.4"
threadpool = "1.7.1" threadpool = "1.7.1"
gophermap = "0.1.2" alphanumeric-sort = "1.4"
shell-escape = "0.1.4"
[package.metadata.release]
pre-release-replacements = [
{file="README.md", search="phd-v\\d\\.\\d\\.\\d-", replace="{{crate_name}}-v{{version}}-"},
{file="README.md", search="/v\\d\\.\\d\\.\\d/", replace="/v{{version}}/"},
]
dev-version-ext = "dev"

View File

@ -27,11 +27,22 @@ uninstall: $(RELEASE)
clean: clean:
-rm -rf target -rm -rf target
# Build and strip the release version # Build the release version
$(RELEASE): $(SOURCES) $(RELEASE): $(SOURCES)
cargo build --release cargo build --release
strip $@
# Build the debug version # Build the debug version
$(DEBUG): $(SOURCES) $(DEBUG): $(SOURCES)
cargo build cargo build
# Build manual
.PHONY: manual
manual: doc/phd.1
doc/phd.1: doc/phd.1.md scdoc
scdoc < doc/phd.1.md > doc/phd.1
# Must have scdoc installed to build manual.
scdoc:
@which scdoc || (echo "scdoc(1) not found."; \
echo "please install to build the manual: https://repology.org/project/scdoc"; exit 1)

175
README.md
View File

@ -4,50 +4,82 @@
| )| )| ) | )| )| )
|__/ | / |__/ |__/ | / |__/
| |
--> <p align="center"><img src="./img/logo.png"></p> -->
<p align="center">
<img src="./img/logo.png"> <br>
`phd` is an esoteric gopher server for small gopherholes. <a href="https://github.com/xvxx/phd/releases">
<img src="https://img.shields.io/github/v/release/xvxx/phd?include_prereleases">
</a>
<a href="https://crates.io/crates/phd">
<img src="https://img.shields.io/crates/v/phd">
</a>
<a href="https://github.com/xvxx/phd/actions?query=workflow%3Abuild">
<img src="https://github.com/xvxx/phd/workflows/build/badge.svg">
</a>
</p>
point it at a directory and it'll serve up all its text files, ---
sub-directories, and binary files over gopher. any `.gph` files will
be served up as [gopermaps][map] and executable `.gph` files will be
run as a script with their output served to the client, like cgi!
special files: `phd` is a small, easy-to-use gopher server.
- **header.gph**: if it exists in a directory, its content will be Point it at a directory and it'll serve up all the text files,
shown above the directory's content. put ascii art in it. sub-directories, and binary files over Gopher. Any `.gph` files will
- **footer.gph**: same, but will be shown below a directory's content. be served up as [gophermaps][map] and executable `.gph` files will be
- **index.gph**: completely replaces a directory's content with what's run as a program with their output served to the client, like the
glorious cgi-bin days of yore!
### ~ special files ~
- **`header.gph`**: If it exists in a directory, its content will be
shown above the directory's content. Put ASCII art in it.
- **`footer.gph`**: Same, but will be shown below a directory's content.
- **`index.gph`**: Completely replaces a directory's content with what's
in this file. in this file.
- **??.gph**: visiting gopher://yoursite/1/dog/ will try to render - **`??.gph`**: Visiting `gopher://yoursite/1/dog/` will try to render
`dog.gph` from disk. `dog.gph` from disk. Visiting `/1/dog.gph` will render the raw
- **.reverse**: if this exists, the directory contents will be listed content of the .gph file.
in reverse alphanumeric order. useful for phloggin'. - **`.reverse`**: If this exists, the directory contents will be listed
in reverse alphanumeric order. Useful for phloggin', if you date
your posts.
any line in a `.gph` file that doesn't contain tabs (`\t`) and doesn't Any line in a `.gph` file that doesn't contain tabs (`\t`) will get an
start with an `i` will get an `i` automatically prefixed, turning it `i` automatically prefixed, turning it into a Gopher information item.
into a gopher information item.
any `.gph` file that is marked **executable** with be run as if it For your convenience, phd supports **[geomyidae][gmi]** syntax for
were a shell script and its output will be sent to the client. it will creating links:
be passed three arguments: the query string (if any, the host, and the
port. do with them what you will.
for example: This is an info line.
[1|This is a link|/help|server|port]
[h|URL Link|URL:https://noogle.com]
`server` and `port` will get translated into the server and port of
the actively running server, eg `localhost` and `7070`.
Any line containing a tab character (`\t`) will be sent as-is to the
client, meaning you can write and serve up raw Gophermap files too.
### ~ dynamic content ~
Any `.gph` file that is marked **executable** with be run as if it
were a standalone program and its output will be sent to the client.
It will be passed three arguments: the query string (if any), the
server's hostname, and the current port. Do with them what you will.
For example:
$ cat echo.gph $ cat echo.gph
#!/bin/sh #!/bin/sh
echo "Hi, world! You said:" $1 echo "Hi, world! You said:" $1
echo "1Visit Gopherpedia / gopherpedia.com 70" echo "1Visit Gopherpedia / gopherpedia.com 70"
then: Then:
$ gopher-client gopher://localhost/1/echo?something $ gopher-client gopher://localhost/1/echo?something
[INFO] Hi, world! You said: something [INFO] Hi, world! You said: something
[LINK] Visit Gopherpedia [LINK] Visit Gopherpedia
or more seriously: Or more seriously:
$ cat figlet.gph $ cat figlet.gph
#!/bin/sh #!/bin/sh
@ -61,10 +93,44 @@ then:
[INFO] | '_ \| | / _` |/ _ \| '_ \| '_ \ / _ \ '__| [INFO] | '_ \| | / _` |/ _ \| '_ \| '_ \ / _ \ '__|
[INFO] | | | | | | (_| | (_) | |_) | | | | __/ | [INFO] | | | | | | (_| | (_) | |_) | | | | __/ |
[INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_| [INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_|
[INFO] |___/ |_| [INFO] |___/ |_|
### ~ ruby on rails ~
## usage `sh` is fun, but for serious work you need a serious scripting
language like Ruby or PHP or Node.JS:
$ cat sizes.gph
#!/usr/bin/env ruby
def filesize(file)
(size=File.size file) > (k=1024) ? "#{size/k}K" : "#{size}B"
end
puts "~ file sizes ~"
spaces = 20
Dir[__dir__ + "/*"].each do |entry|
name = File.basename entry
puts "#{name}#{' ' * (spaces - name.length)}#{filesize entry}"
end
Now you can finally share the file sizes of a directory with the world
of Gopher!
$ phetch -r 0.0.0.0:7070/1/sizes
i~ file sizes ~ (null) 127.0.0.1 7070
iCargo.toml 731B (null) 127.0.0.1 7070
iLICENSE 1K (null) 127.0.0.1 7070
iMakefile 724B (null) 127.0.0.1 7070
itarget 288B (null) 127.0.0.1 7070
iphd 248K (null) 127.0.0.1 7070
iCargo.lock 2K (null) 127.0.0.1 7070
iREADME.md 4K (null) 127.0.0.1 7070
img 96B (null) 127.0.0.1 7070
isizes.gph 276B (null) 127.0.0.1 7070
isrc 224B (null) 127.0.0.1 7070
## ~ usage ~
Usage: Usage:
@ -72,8 +138,11 @@ then:
Options: Options:
-p, --port Port to bind to. -r, --render SELECTOR Render and print SELECTOR to stdout only.
-h, --host Hostname to use when generating links. -h, --host HOST Hostname for links. [Default: {host}]
-p, --port PORT Port for links. [Default: {port}]
-b, --bind ADDRESS Socket address to bind to. [Default: {bind}]
--no-color Don't show colors in log messages.
Other flags: Other flags:
@ -85,38 +154,52 @@ then:
phd ./path/to/site # Serve directory over port 7070. phd ./path/to/site # Serve directory over port 7070.
phd -p 70 docs # Serve 'docs' directory on port 70 phd -p 70 docs # Serve 'docs' directory on port 70
phd -h gopher.com # Serve current directory over port 7070 phd -h gopher.com # Serve current directory over port 7070
# using hostname "gopher.com" # using hostname 'gopher.com'
phd -r / ./site # Render local gopher site to stdout.
## installation ## ~ installation ~
binaries for linux, mac, and raspberry pi are available On macOS you can install with [Homebrew](https://brew.sh/):
at https://github.com/dvkt/phd/releases:
- [phd-v0.1.5-linux-x86_64.tar.gz][0] brew install xvxx/code/phd
- [phd-v0.1.5-linux-armv7.tar.gz (RPi)][1]
- [phd-v0.1.5-macos.zip][2]
just unzip/untar the `phd` program into your $PATH and get going! Binaries for Linux, Mac, and Raspberry Pi are available at
gopher://phkt.io/1/releases/phd and https://github.com/xvxx/phd/releases:
## development - [phd-v0.1.15-linux-x86_64.tar.gz][0]
- [phd-v0.1.15-linux-armv7.tar.gz (Raspberry Pi)][1]
- [phd-v0.1.15-macos.zip][2]
Just unzip/untar the `phd` program into your `$PATH` and get going!
If you have **[cargo][rustup]**, you can install the crate directly:
cargo install phd --locked
## ~ development ~
cargo run -- ./path/to/gopher/site cargo run -- ./path/to/gopher/site
## resources ## ~ resources ~
- gopher://bitreich.org/1/scm/geomyidae/files.gph
- https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap - https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap
- https://gopher.zone/posts/how-to-gophermap/ - https://gopher.zone/posts/how-to-gophermap/
- [rfc 1436](https://tools.ietf.org/html/rfc1436) - [rfc 1436](https://tools.ietf.org/html/rfc1436)
## todo ## ~ todo ~
- [ ] script/serverless mode
- [ ] systemd config, or something - [ ] systemd config, or something
- [ ] TLS support - [ ] TLS support
- [ ] man page - [ ] user input sanitization tests
- [ ] ipv6
[0]: https://github.com/dvkt/phd/releases/download/v0.1.5/phd-v0.1.5-linux-x86_64.tar.gz ## ~ status ~
[1]: https://github.com/dvkt/phd/releases/download/v0.1.5/phd-v0.1.5-linux-armv7.tar.gz
[2]: https://github.com/dvkt/phd/releases/download/v0.1.5/phd-v0.1.5-macos.zip phd is no longer under active development, but the latest version works great.
[0]: https://github.com/xvxx/phd/releases/download/v0.1.15/phd-v0.1.15-linux-x86_64.tar.gz
[1]: https://github.com/xvxx/phd/releases/download/v0.1.15/phd-v0.1.15-linux-armv7.tar.gz
[2]: https://github.com/xvxx/phd/releases/download/v0.1.15/phd-v0.1.15-macos.zip
[map]: https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu [map]: https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu
[gmi]: http://r-36.net/scm/geomyidae/
[rustup]: https://rustup.rs

206
doc/phd.1 Normal file
View File

@ -0,0 +1,206 @@
.\" Generated by scdoc 1.11.0
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "PHD" "1" "2020-06-27"
.P
.SH NAME
.P
phd - an estoeric gopher server
.P
.SH SYNOPSIS
.P
\fBphd\fR [\fIOPTIONS\fR] [\fISITE ROOT\fR]
.P
.SH DESCRIPTION
.P
\fBphd\fR is a small, easy-to-use gopher server.
.P
Point it at a directory and it'll serve up all the text files,
sub-directories, and binary files over Gopher. Any \fB.gph\fR files will
be served up as Gophermaps and executable \fB.gph\fR files will be
run as a program with their output served to the client, like the
glorious cgi-bin days of yore!
.P
Usually \fBphd\fR is started with a path to your Gopher site:
.P
.RS 4
phd /srv/gopher
.P
.RE
If no path is given, \fBphd\fR will use the current directory as the root
of your Gopher site.
.P
.SH OPTIONS
.P
\fB-r\fR \fISELECTOR\fR, \fB--render\fR \fISELECTOR\fR
.P
.RS 4
Rather than start as a server, render the \fISELECTOR\fR of the site using the options provided and print the raw response to \fBSTDOUT\fR.
.P
.RE
\fB-b\fR \fIADDRESS\fR, \fB--bind\fR \fIADDRESS\fR
.RS 4
Set the socket address to bind to, e.g. \fB127.0.0.1:7070\fR
.P
.RE
\fB-p\fR \fIPORT\fR, \fB--port\fR \fIPORT\fR
.RS 4
Set the \fIPORT\fR to use when generating Gopher links.
.P
.RE
\fB-h\fR \fIHOST\fR, \fB--host\fR \fIHOST\fR
.RS 4
Set the \fIHOST\fR to use when generating Gopher links.
.P
.RE
\fB-h\fR, \fB--help\fR
.RS 4
Print a help summary and exit.
.P
.RE
\fB-v\fR, \fB--version\fR
.RS 4
Print version information and exit.
.P
.RE
.SH SPECIAL FILES
.P
The following files have special behavior when present in a directory
that \fBphd\fR is tasked with serving:
.P
\fBheader.gph\fR
.RS 4
If it exists in a directory, its content will be shown above the directory's content. Put ASCII art in it.
.P
.RE
\fBfooter.gph\fR
.RS 4
Same, but will be shown below a directory's content.
.P
.RE
\fBindex.gph\fR
.RS 4
Completely replaces a directory's content with what's in this file.
.P
.RE
\fB??.gph\fR
.RS 4
Visiting \fBgopher://yoursite/1/dog/\fR will try to render \fBdog.gph\fR from disk. Visiting \fB/1/dog.gph\fR will render the raw content of the .gph file.
.P
.RE
\fB.reverse\fR
.RS 4
If this exists, the directory contents will be listed in reverse alphanumeric order. Useful for phloggin', if you date your posts.
.P
.RE
.SH GOPHERMAP SYNTAX
.P
Any line in a \fB.gph\fR file that doesn't contain tabs (\fBt\fR) will get an
\fBi\fR automatically prefixed, turning it into a Gopher information item.
.P
For your convenience, phd supports \fBgeomyidae\fR syntax for
creating links:
.P
.nf
.RS 4
This is an info line\&.
[1|This is a link|/help|server|port]
[h|URL Link|URL:https://noogle\&.com]
.fi
.RE
.P
\fBserver\fR and \fBport\fR will get translated into the server and port of
the actively running server, eg \fBlocalhost\fR and \fB7070\fR.
.P
Any line containing a tab character (\fBt\fR) will be sent as-is to the
client, meaning you can write and serve up raw Gophermap files too.
.P
.SH DYNAMIC CONTENT
.P
Any \fB.gph\fR file that is marked \fBexecutable\fR with be run as if it
were a standalone program and its output will be sent to the client.
It will be passed three arguments: the query string (if any), the
server's hostname, and the current port. Do with them what you will.
.P
For example:
.P
.nf
.RS 4
$ cat echo\&.gph
#!/bin/sh
echo "Hi, world! You said:" $1
echo "1Visit Gopherpedia / gopherpedia\&.com 70"
.fi
.RE
.P
Then:
.P
.nf
.RS 4
$ gopher-client gopher://localhost/1/echo?something
[INFO] Hi, world! You said: something
[LINK] Visit Gopherpedia
.fi
.RE
.P
Or more seriously:
.P
.nf
.RS 4
$ cat figlet\&.gph
#!/bin/sh
figlet $1
.fi
.RE
.P
then:
.P
.nf
.RS 4
$ gopher-client gopher://localhost/1/figlet?hi gopher
[INFO] _ _ _
[INFO] | |__ (_) __ _ ___ _ __ | |__ ___ _ __
[INFO] | '_ | | / _` |/ _ | '_ | '_ / _ '__|
[INFO] | | | | | | (_| | (_) | |_) | | | | __/ |
[INFO] |_| |_|_| __, |___/| \&.__/|_| |_|___|_|
[INFO] |___/ |_|
.fi
.RE
.P
.SS RESOURCES
.P
geomyidae source code
.RS 4
gopher://bitreich.org/1/scm/geomyidae/files.gph
.P
.RE
Example Gophermap
.RS 4
https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap
.P
.RE
Gophermaps
.RS 4
https://gopher.zone/posts/how-to-gophermap/
.P
.RE
RFC 1436:
.RS 4
https://tools.ietf.org/html/rfc1436
.P
.RE
.SH ABOUT
.P
\fBphd\fR is maintained by chris west and released under the MIT license.
.P
phd's Gopher hole:
.RS 4
\fIgopher://phkt.io/1/phd\fR
.RE
phd's webpage:
.RS 4
\fIhttps://github.com/xvxx/phd\fR

154
doc/phd.1.md Normal file
View File

@ -0,0 +1,154 @@
PHD(1)
# NAME
phd - an estoeric gopher server
# SYNOPSIS
*phd* [_OPTIONS_] [_SITE ROOT_]
# DESCRIPTION
*phd* is a small, easy-to-use gopher server.
Point it at a directory and it'll serve up all the text files,
sub-directories, and binary files over Gopher. Any *.gph* files will
be served up as Gophermaps and executable *.gph* files will be
run as a program with their output served to the client, like the
glorious cgi-bin days of yore!
Usually *phd* is started with a path to your Gopher site:
phd /srv/gopher
If no path is given, *phd* will use the current directory as the root
of your Gopher site.
# OPTIONS
*-r* _SELECTOR_, *--render* _SELECTOR_
Rather than start as a server, render the _SELECTOR_ of the site using the options provided and print the raw response to *STDOUT*.
*-b* _ADDRESS_, *--bind* _ADDRESS_
Set the socket address to bind to, e.g. *127.0.0.1:7070*
*-p* _PORT_, *--port* _PORT_
Set the _PORT_ to use when generating Gopher links.
*-h* _HOST_, *--host* _HOST_
Set the _HOST_ to use when generating Gopher links.
*-h*, *--help*
Print a help summary and exit.
*-v*, *--version*
Print version information and exit.
# SPECIAL FILES
The following files have special behavior when present in a directory
that *phd* is tasked with serving:
*header.gph*
If it exists in a directory, its content will be shown above the directory's content. Put ASCII art in it.
*footer.gph*
Same, but will be shown below a directory's content.
*index.gph*
Completely replaces a directory's content with what's in this file.
*??.gph*
Visiting *gopher://yoursite/1/dog/* will try to render *dog.gph* from disk. Visiting */1/dog.gph* will render the raw content of the .gph file.
*.reverse*
If this exists, the directory contents will be listed in reverse alphanumeric order. Useful for phloggin', if you date your posts.
# GOPHERMAP SYNTAX
Any line in a *.gph* file that doesn't contain tabs (*\t*) will get an
*i* automatically prefixed, turning it into a Gopher information item.
For your convenience, phd supports *geomyidae* syntax for
creating links:
```
This is an info line.
[1|This is a link|/help|server|port]
[h|URL Link|URL:https://noogle.com]
```
*server* and *port* will get translated into the server and port of
the actively running server, eg *localhost* and *7070*.
Any line containing a tab character (*\t*) will be sent as-is to the
client, meaning you can write and serve up raw Gophermap files too.
# DYNAMIC CONTENT
Any *.gph* file that is marked *executable* with be run as if it
were a standalone program and its output will be sent to the client.
It will be passed three arguments: the query string (if any), the
server's hostname, and the current port. Do with them what you will.
For example:
```
$ cat echo.gph
#!/bin/sh
echo "Hi, world! You said:" $1
echo "1Visit Gopherpedia / gopherpedia.com 70"
```
Then:
```
$ gopher-client gopher://localhost/1/echo?something
[INFO] Hi, world! You said: something
[LINK] Visit Gopherpedia
```
Or more seriously:
```
$ cat figlet.gph
#!/bin/sh
figlet $1
```
then:
```
$ gopher-client gopher://localhost/1/figlet?hi gopher
[INFO] _ _ _
[INFO] | |__ (_) __ _ ___ _ __ | |__ ___ _ __
[INFO] | '_ \| | / _` |/ _ \| '_ \| '_ \ / _ \ '__|
[INFO] | | | | | | (_| | (_) | |_) | | | | __/ |
[INFO] |_| |_|_| \__, |\___/| .__/|_| |_|\___|_|
[INFO] |___/ |_|
```
## RESOURCES
geomyidae source code
gopher://bitreich.org/1/scm/geomyidae/files.gph
Example Gophermap
https://github.com/gophernicus/gophernicus/blob/master/README.Gophermap
Gophermaps
https://gopher.zone/posts/how-to-gophermap/
RFC 1436:
https://tools.ietf.org/html/rfc1436
# ABOUT
*phd* is maintained by chris west and released under the MIT license.
phd's Gopher hole:
_gopher://phkt.io/1/phd_
phd's webpage:
_https://github.com/xvxx/phd_

10
header.gph Normal file
View File

@ -0,0 +1,10 @@
/ |
___ (___ ___|
| )| )| )
|__/ | / |__/
|
~ browse source ~
(updated nightly)

View File

@ -1,11 +1,40 @@
use std::fmt; //! Cheesy way to easily wrap text in console colors.
//! Example:
//! ```
//! use phd::color;
//! println!("{}Error: {}{}", color::Red, "Something broke.", color::Reset);
//! ```
use std::{
fmt,
sync::atomic::{AtomicBool, Ordering as AtomicOrdering},
};
/// Whether to show colors or not.
/// Defaults to true.
static SHOW_COLORS: AtomicBool = AtomicBool::new(true);
/// Hide colors.
pub fn hide_colors() {
SHOW_COLORS.swap(false, AtomicOrdering::Relaxed);
}
/// Are we showing colors are not?
pub fn showing_colors() -> bool {
SHOW_COLORS.load(AtomicOrdering::Relaxed)
}
macro_rules! color { macro_rules! color {
($t:ident, $code:expr) => { ($t:ident, $code:expr) => {
#[allow(missing_docs)]
pub struct $t; pub struct $t;
impl fmt::Display for $t { impl fmt::Display for $t {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\x1b[{}m", $code) if showing_colors() {
write!(f, "\x1b[{}m", $code)
} else {
write!(f, "")
}
} }
} }
}; };

127
src/gopher.rs Normal file
View File

@ -0,0 +1,127 @@
//! Gopher type "borrowed" from phetch.
use std::fmt;
/// Gopher types are defined according to RFC 1436.
#[allow(missing_docs)]
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Type {
Text, // 0
Menu, // 1
CSOEntity, // 2
Error, // 3
Binhex, // 4
DOSFile, // 5
UUEncoded, // 6
Search, // 7
Telnet, // 8
Binary, // 9
Mirror, // +
GIF, // g
Telnet3270, // T
HTML, // h
Image, // I
PNG, // p
Info, // i
Sound, // s
Document, // d
}
impl Type {
/// Is this an info line?
pub fn is_info(self) -> bool {
self == Type::Info
}
/// Text document?
pub fn is_text(self) -> bool {
self == Type::Text
}
/// HTML link?
pub fn is_html(self) -> bool {
self == Type::HTML
}
/// Telnet link?
pub fn is_telnet(self) -> bool {
self == Type::Telnet
}
/// Is this a link, ie something we can navigate to or open?
pub fn is_link(self) -> bool {
!self.is_info()
}
/// Is this something we can download?
pub fn is_download(self) -> bool {
match self {
Type::Binhex
| Type::DOSFile
| Type::UUEncoded
| Type::Binary
| Type::GIF
| Type::Image
| Type::PNG
| Type::Sound
| Type::Document => true,
_ => false,
}
}
/// Gopher Item Type to RFC char.
pub fn to_char(self) -> char {
match self {
Type::Text => '0',
Type::Menu => '1',
Type::CSOEntity => '2',
Type::Error => '3',
Type::Binhex => '4',
Type::DOSFile => '5',
Type::UUEncoded => '6',
Type::Search => '7',
Type::Telnet => '8',
Type::Binary => '9',
Type::Mirror => '+',
Type::GIF => 'g',
Type::Telnet3270 => 'T',
Type::HTML => 'h',
Type::Image => 'I',
Type::PNG => 'p',
Type::Info => 'i',
Type::Sound => 's',
Type::Document => 'd',
}
}
/// Create a Gopher Item Type from its RFC char code.
pub fn from(c: char) -> Option<Type> {
Some(match c {
'0' => Type::Text,
'1' => Type::Menu,
'2' => Type::CSOEntity,
'3' => Type::Error,
'4' => Type::Binhex,
'5' => Type::DOSFile,
'6' => Type::UUEncoded,
'7' => Type::Search,
'8' => Type::Telnet,
'9' => Type::Binary,
'+' => Type::Mirror,
'g' => Type::GIF,
'T' => Type::Telnet3270,
'h' => Type::HTML,
'I' => Type::Image,
'p' => Type::PNG,
'i' => Type::Info,
's' => Type::Sound,
'd' => Type::Document,
_ => return None,
})
}
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_char())
}
}

View File

@ -1,7 +1,21 @@
//! phd is a small, easy-to-use Gopher server that tries to make
//! serving up a Gopher site quick and painless. Best used for local
//! development or low traffic Gopher sites.
#![allow(unused_must_use)]
#![warn(absolute_paths_not_starting_with_crate)]
#![warn(explicit_outlives_requirements)]
#![warn(unreachable_pub)]
#![warn(deprecated_in_future)]
#![warn(missing_docs)]
#![allow(clippy::while_let_on_iterator)]
pub mod color; pub mod color;
pub mod gopher;
pub mod request; pub mod request;
pub mod server; pub mod server;
pub use crate::request::Request; pub use crate::request::Request;
/// Alias for a generic Result type.
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

View File

@ -1,21 +1,38 @@
use phd; use phd;
use std::process; use std::process;
const DEFAULT_BIND: &str = "[::]:7070";
const DEFAULT_HOST: &str = "127.0.0.1"; const DEFAULT_HOST: &str = "127.0.0.1";
const DEFAULT_PORT: u16 = 7070; const DEFAULT_PORT: u16 = 7070;
fn main() { fn main() {
let args: Vec<String> = std::env::args().skip(1).collect(); let args = std::env::args().skip(1).collect::<Vec<_>>();
let mut args = args.iter();
let mut root = "."; let mut root = ".";
let mut iter = args.iter(); let mut addr = DEFAULT_BIND;
let mut host = DEFAULT_HOST; let mut host = DEFAULT_HOST;
let mut port = DEFAULT_PORT; let mut port = DEFAULT_PORT;
while let Some(arg) = iter.next() { let mut render = "";
while let Some(arg) = args.next() {
match arg.as_ref() { match arg.as_ref() {
"--version" | "-v" | "-version" => return print_version(), "--version" | "-v" | "-version" => return print_version(),
"--help" | "-help" => return print_help(), "--help" | "-help" => return print_help(),
"--no-color" | "-no-color" => phd::color::hide_colors(),
"--render" | "-render" | "-r" => {
if let Some(path) = args.next() {
render = path;
} else {
render = "/";
}
}
"--bind" | "-b" | "-bind" => {
if let Some(a) = args.next() {
addr = a
}
}
"--port" | "-p" | "-port" => { "--port" | "-p" | "-port" => {
if let Some(p) = iter.next() { if let Some(p) = args.next() {
port = p port = p
.parse() .parse()
.map_err(|_| { .map_err(|_| {
@ -26,15 +43,15 @@ fn main() {
} }
} }
"-h" => { "-h" => {
if let Some(h) = iter.next() { if let Some(h) = args.next() {
host = h; host = &h;
} else { } else {
return print_help(); return print_help();
} }
} }
"--host" | "-host" => { "--host" | "-host" => {
if let Some(h) = iter.next() { if let Some(h) = args.next() {
host = h; host = &h;
} }
} }
_ => { _ => {
@ -42,13 +59,32 @@ fn main() {
eprintln!("unknown flag: {}", arg); eprintln!("unknown flag: {}", arg);
process::exit(1); process::exit(1);
} else { } else {
root = arg; root = &arg;
} }
} }
} }
} }
if let Err(e) = phd::server::start(host, port, root) { // https://no-color.org/
if std::env::var("NO_COLOR").is_ok() {
phd::color::hide_colors()
}
// If port was given and socket wasn't, bind to that port.
let bind = if port != DEFAULT_PORT && addr == DEFAULT_BIND {
format!("[::]:{}", port).parse().unwrap()
} else {
addr.parse().unwrap()
};
if !render.is_empty() {
return match phd::server::render(host, port, root, &render) {
Ok(out) => print!("{}", out),
Err(e) => eprintln!("{}", e),
};
}
if let Err(e) = phd::server::start(bind, host, port, root) {
eprintln!("{}", e); eprintln!("{}", e);
} }
} }
@ -61,15 +97,28 @@ fn print_help() {
Options: Options:
-p, --port Port to bind to. [Default: {port}] -r, --render SELECTOR Render and print SELECTOR to stdout only.
-h, --host Hostname when generating links. [Default: {host}] -h, --host HOST Hostname for links. [Default: {host}]
-p, --port PORT Port for links. [Default: {port}]
-b, --bind ADDRESS Socket address to bind to. [Default: {bind}]
--no-color Don't show colors in log messages.
Other flags: Other flags:
-h, --help Print this screen. -h, --help Print this screen.
-v, --version Print phd version.", -v, --version Print phd version.
Examples:
phd ./path/to/site # Serve directory over port 7070.
phd -p 70 docs # Serve 'docs' directory on port 70
phd -h gopher.com # Serve current directory over port 7070
# using hostname 'gopher.com'
phd -r / ./site # Render local gopher site to stdout.
",
host = DEFAULT_HOST, host = DEFAULT_HOST,
port = DEFAULT_PORT, port = DEFAULT_PORT,
bind = DEFAULT_BIND,
); );
} }

View File

@ -1,13 +1,23 @@
//! A Request represents a Gopher request made by a client. phd can
//! serve directory listings as Gopher Menus, plain text files as
//! Text, binary files as downloads, Gophermap files as menus, or
//! executable files as dynamic content.
use crate::Result; use crate::Result;
use std::fs; use std::fs;
/// This struct represents a single gopher request. /// This struct represents a single gopher request.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Request { pub struct Request {
/// Gopher selector requested
pub selector: String, pub selector: String,
/// Search query string, if any.
pub query: String, pub query: String,
/// Root directory of the server. Can't serve outside of this.
pub root: String, pub root: String,
/// Host of the currently running server.
pub host: String, pub host: String,
/// Port of the currently running server.
pub port: u16, pub port: u16,
} }
@ -16,7 +26,7 @@ impl Request {
pub fn from(host: &str, port: u16, root: &str) -> Result<Request> { pub fn from(host: &str, port: u16, root: &str) -> Result<Request> {
Ok(Request { Ok(Request {
host: host.into(), host: host.into(),
port: port, port,
root: fs::canonicalize(root)?.to_string_lossy().into(), root: fs::canonicalize(root)?.to_string_lossy().into(),
selector: String::new(), selector: String::new(),
query: String::new(), query: String::new(),
@ -25,12 +35,11 @@ impl Request {
/// Path to the target file on disk requested by this request. /// Path to the target file on disk requested by this request.
pub fn file_path(&self) -> String { pub fn file_path(&self) -> String {
let mut path = self.root.to_string(); format!(
if !path.ends_with('/') { "{}/{}",
path.push('/'); self.root.to_string().trim_end_matches('/'),
} self.selector.replace("..", ".").trim_start_matches('/')
path.push_str(self.selector.replace("..", ".").trim_start_matches('/')); )
path
} }
/// Path to the target file relative to the server root. /// Path to the target file relative to the server root.
@ -42,8 +51,12 @@ impl Request {
pub fn parse_request(&mut self, line: &str) { pub fn parse_request(&mut self, line: &str) {
self.query.clear(); self.query.clear();
self.selector.clear(); self.selector.clear();
if let Some(i) = line.find('\t') { if let Some((i, _)) = line
if line.len() >= i + 1 { .chars()
.enumerate()
.find(|&(_, c)| c == '\t' || c == '?')
{
if line.len() > i {
self.query.push_str(&line[i + 1..]); self.query.push_str(&line[i + 1..]);
self.selector.push_str(&line[..i]); self.selector.push_str(&line[..i]);
return; return;

View File

@ -1,13 +1,16 @@
use crate::{color, Request, Result}; //! A simple multi-threaded Gopher server.
use gophermap::{GopherMenu, ItemType};
use crate::{color, gopher, Request, Result};
use std::{ use std::{
fs, cmp::Ordering,
fs::{self, DirEntry},
io::{self, prelude::*, BufReader, Read, Write}, io::{self, prelude::*, BufReader, Read, Write},
net::{TcpListener, TcpStream}, net::{SocketAddr, TcpListener, TcpStream},
os::unix::fs::PermissionsExt, os::unix::fs::PermissionsExt,
path::Path, path::Path,
process::Command, process::Command,
str, str,
sync::atomic::{AtomicBool, Ordering as AtomicOrdering},
}; };
use threadpool::ThreadPool; use threadpool::ThreadPool;
@ -21,19 +24,42 @@ const MAX_PEEK_SIZE: usize = 1024;
/// Files not displayed in directory listings. /// Files not displayed in directory listings.
const IGNORED_FILES: [&str; 3] = ["header.gph", "footer.gph", ".reverse"]; const IGNORED_FILES: [&str; 3] = ["header.gph", "footer.gph", ".reverse"];
/// Whether to print info!() messages to stdout.
/// Defaults to true.
static SHOW_INFO: AtomicBool = AtomicBool::new(true);
/// Hide info! messages.
fn hide_info() {
SHOW_INFO.swap(false, AtomicOrdering::Relaxed);
}
/// Print status message to the server's stdout.
macro_rules! info {
($e:expr) => {
if SHOW_INFO.load(AtomicOrdering::Relaxed) {
println!("{}", $e);
}
};
($fmt:expr, $($args:expr),*) => {
info!(format!($fmt, $($args),*));
};
($fmt:expr, $($args:expr,)*) => {
info!(format!($fmt, $($args,)*));
};
}
/// Starts a Gopher server at the specified host, port, and root directory. /// Starts a Gopher server at the specified host, port, and root directory.
pub fn start(host: &str, port: u16, root: &str) -> Result<()> { pub fn start(bind: SocketAddr, host: &str, port: u16, root: &str) -> Result<()> {
let addr = format!("{}:{}", "0.0.0.0", port); let listener = TcpListener::bind(&bind)?;
let listener = TcpListener::bind(&addr)?;
let full_root_path = fs::canonicalize(&root)?.to_string_lossy().to_string(); let full_root_path = fs::canonicalize(&root)?.to_string_lossy().to_string();
let pool = ThreadPool::new(MAX_WORKERS); let pool = ThreadPool::new(MAX_WORKERS);
println!( info!(
"{}┬ Listening {}on {}{}{} at {}{}{}", "{}» Listening {}on {}{}{} at {}{}{}",
color::Yellow, color::Yellow,
color::Reset, color::Reset,
color::Yellow, color::Yellow,
addr, bind,
color::Reset, color::Reset,
color::Blue, color::Blue,
full_root_path, full_root_path,
@ -41,7 +67,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
); );
for stream in listener.incoming() { for stream in listener.incoming() {
let stream = stream?; let stream = stream?;
println!( info!(
"{}┌ Connection{} from {}{}", "{}┌ Connection{} from {}{}",
color::Green, color::Green,
color::Reset, color::Reset,
@ -51,7 +77,7 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
let req = Request::from(host, port, root)?; let req = Request::from(host, port, root)?;
pool.execute(move || { pool.execute(move || {
if let Err(e) = accept(stream, req) { if let Err(e) = accept(stream, req) {
eprintln!("{}{}{}", color::Red, e, color::Reset); info!("{}└ {}{}", color::Red, e, color::Reset);
} }
}); });
} }
@ -59,11 +85,11 @@ pub fn start(host: &str, port: u16, root: &str) -> Result<()> {
} }
/// Reads from the client and responds. /// Reads from the client and responds.
fn accept(stream: TcpStream, mut req: Request) -> Result<()> { fn accept(mut stream: TcpStream, mut req: Request) -> Result<()> {
let reader = BufReader::new(&stream); let reader = BufReader::new(&stream);
let mut lines = reader.lines(); let mut lines = reader.lines();
if let Some(Ok(line)) = lines.next() { if let Some(Ok(line)) = lines.next() {
println!( info!(
"{}│{} Client sent:\t{}{:?}{}", "{}│{} Client sent:\t{}{:?}{}",
color::Green, color::Green,
color::Reset, color::Reset,
@ -72,15 +98,25 @@ fn accept(stream: TcpStream, mut req: Request) -> Result<()> {
color::Reset color::Reset
); );
req.parse_request(&line); req.parse_request(&line);
write_response(&stream, req)?; write_response(&mut stream, req)?;
} }
Ok(()) Ok(())
} }
/// Render a response to a String.
pub fn render(host: &str, port: u16, root: &str, selector: &str) -> Result<String> {
hide_info();
let mut req = Request::from(host, port, root)?;
req.parse_request(&selector);
let mut out = vec![];
write_response(&mut out, req)?;
Ok(String::from_utf8_lossy(&out).into())
}
/// Writes a response to a client based on a Request. /// Writes a response to a client based on a Request.
fn write_response<'a, W>(w: &'a W, mut req: Request) -> Result<()> fn write_response<W>(w: &mut W, mut req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
@ -118,9 +154,9 @@ where
} }
/// Send a directory listing (menu) to the client based on a Request. /// Send a directory listing (menu) to the client based on a Request.
fn write_dir<'a, W>(w: &'a W, req: Request) -> Result<()> fn write_dir<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
if !fs_exists(&path) { if !fs_exists(&path) {
@ -141,34 +177,26 @@ where
)?; )?;
} }
let mut menu = GopherMenu::with_write(w);
let rel_path = req.relative_file_path(); let rel_path = req.relative_file_path();
// sort directory entries // show directory entries
let mut paths: Vec<_> = fs::read_dir(&path)?.filter_map(|r| r.ok()).collect(); let reverse = format!("{}/.reverse", path);
let mut reverse = path.clone(); let paths = sort_paths(&path, fs_exists(&reverse))?;
reverse.push_str("/.reverse");
let is_dir = |entry: &fs::DirEntry| match entry.file_type() {
Ok(t) => t.is_dir(),
_ => false,
};
if fs_exists(&reverse) {
paths.sort_by_key(|entry| (!is_dir(&entry), std::cmp::Reverse(entry.path())));
} else {
paths.sort_by_key(|entry| (!is_dir(&entry), entry.path()));
}
for entry in paths { for entry in paths {
let file_name = entry.file_name(); let file_name = entry.file_name();
let f = file_name.to_string_lossy().to_string(); let f = file_name.to_string_lossy().to_string();
if f.chars().nth(0) == Some('.') || IGNORED_FILES.contains(&f.as_ref()) { if f.chars().nth(0) == Some('.') || IGNORED_FILES.contains(&f.as_ref()) {
continue; continue;
} }
let mut path = rel_path.clone(); let path = format!(
path.push('/'); "{}/{}",
path.push_str(&file_name.to_string_lossy()); rel_path.trim_end_matches('/'),
menu.write_entry( file_name.to_string_lossy()
file_type(&entry), );
write!(
w,
"{}{}\t{}\t{}\t{}\r\n",
file_type(&entry).to_char(),
&file_name.to_string_lossy(), &file_name.to_string_lossy(),
&path, &path,
&req.host, &req.host,
@ -176,11 +204,9 @@ where
)?; )?;
} }
let mut footer = path.clone(); let footer = format!("{}/footer.gph", path.trim_end_matches('/'));
footer.push_str("/footer.gph");
if fs_exists(&footer) { if fs_exists(&footer) {
let mut sel = req.selector.clone(); let sel = format!("{}/footer.gph", req.selector);
sel.push_str("/footer.gph");
write_gophermap( write_gophermap(
w, w,
Request { Request {
@ -190,8 +216,9 @@ where
)?; )?;
} }
menu.end()?; write!(w, ".\r\n");
println!(
info!(
"{}│{} Server reply:\t{}DIR {}{}{}", "{}│{} Server reply:\t{}DIR {}{}{}",
color::Green, color::Green,
color::Reset, color::Reset,
@ -204,14 +231,14 @@ where
} }
/// Send a file to the client based on a Request. /// Send a file to the client based on a Request.
fn write_file<'a, W>(mut w: &'a W, req: Request) -> Result<()> fn write_file<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
let mut f = fs::File::open(&path)?; let mut f = fs::File::open(&path)?;
io::copy(&mut f, &mut w)?; io::copy(&mut f, w)?;
println!( info!(
"{}│{} Server reply:\t{}FILE {}{}{}", "{}│{} Server reply:\t{}FILE {}{}{}",
color::Green, color::Green,
color::Reset, color::Reset,
@ -224,9 +251,9 @@ where
} }
/// Send a gophermap (menu) to the client based on a Request. /// Send a gophermap (menu) to the client based on a Request.
fn write_gophermap<'a, W>(mut w: &'a W, req: Request) -> Result<()> fn write_gophermap<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let path = req.file_path(); let path = req.file_path();
@ -238,24 +265,9 @@ where
}; };
for line in reader.lines() { for line in reader.lines() {
let mut line = line.trim_end_matches("\r").to_string(); write!(w, "{}", gph_line_to_gopher(line, &req))?;
match line.chars().filter(|&c| c == '\t').count() {
0 => {
// Insert `i` prefix to any prefix-less lines without tabs.
if line.chars().nth(0) != Some('i') {
line.insert(0, 'i');
}
line.push_str(&format!("\t(null)\t{}\t{}", req.host, req.port))
}
// Auto-add host and port to lines with just a selector.
1 => line.push_str(&format!("\t{}\t{}", req.host, req.port)),
2 => line.push_str(&format!("\t{}", req.port)),
_ => {}
}
line.push_str("\r\n");
w.write_all(line.as_bytes())?;
} }
println!( info!(
"{}│{} Server reply:\t{}MAP {}{}{}", "{}│{} Server reply:\t{}MAP {}{}{}",
color::Green, color::Green,
color::Reset, color::Reset,
@ -267,26 +279,80 @@ where
Ok(()) Ok(())
} }
fn write_not_found<'a, W>(mut w: &'a W, req: Request) -> Result<()> /// Given a single line from a .gph file, convert it into a
/// Gopher-format line. Supports a basic format where lines without \t
/// get an `i` prefixed, and the geomyidae format.
fn gph_line_to_gopher(line: &str, req: &Request) -> String {
if line.starts_with('#') {
return "".to_string();
}
let mut line = line.trim_end_matches('\r').to_string();
if line.starts_with('[') && line.ends_with(']') && line.contains('|') {
// [1|name|sel|server|port]
let port = req.port.to_string();
line = line
.replacen('|', "", 1)
.trim_start_matches('[')
.trim_end_matches(']')
.replace("\\|", "__P_ESC_PIPE") // cheap hack
.replace('|', "\t")
.replace("__P_ESC_PIPE", "\\|")
.replace("\tserver\t", format!("\t{}\t", req.host).as_ref())
.replace("\tport", format!("\t{}", port).as_ref());
let tabs = line.matches('\t').count();
if tabs < 1 {
line.push('\t');
line.push_str("(null)");
}
// if a link is missing host + port, assume it's this server.
// if it's just missing the port, assume port 70
if tabs < 2 {
line.push('\t');
line.push_str(&req.host);
line.push('\t');
line.push_str(&port);
} else if tabs < 3 {
line.push('\t');
line.push_str("70");
}
} else {
match line.matches('\t').count() {
0 => {
// Always insert `i` prefix to any lines without tabs.
line.insert(0, 'i');
line.push_str(&format!("\t(null)\t{}\t{}", req.host, req.port))
}
// Auto-add host and port to lines with just a selector.
1 => line.push_str(&format!("\t{}\t{}", req.host, req.port)),
2 => line.push_str(&format!("\t{}", req.port)),
_ => {}
}
}
line.push_str("\r\n");
line
}
fn write_not_found<W>(w: &mut W, req: Request) -> Result<()>
where where
&'a W: Write, W: Write,
{ {
let line = format!("3Not Found: {}\t/\tnone\t70\r\n", req.selector); let line = format!("3Not Found: {}\t/\tnone\t70\r\n", req.selector);
println!( info!(
"{}│ Not found: {}{}{}", "{}│ Not found: {}{}{}",
color::Red, color::Red,
color::Cyan, color::Cyan,
req.relative_file_path(), req.relative_file_path(),
color::Reset, color::Reset,
); );
w.write_all(line.as_bytes())?; write!(w, "{}", line)?;
Ok(()) Ok(())
} }
/// Determine the gopher type for a DirEntry on disk. /// Determine the gopher type for a DirEntry on disk.
fn file_type(dir: &fs::DirEntry) -> ItemType { fn file_type(dir: &fs::DirEntry) -> gopher::Type {
let metadata = match dir.metadata() { let metadata = match dir.metadata() {
Err(_) => return ItemType::Error, Err(_) => return gopher::Type::Error,
Ok(md) => md, Ok(md) => md,
}; };
@ -295,17 +361,17 @@ fn file_type(dir: &fs::DirEntry) -> ItemType {
let mut buffer: Vec<u8> = vec![]; let mut buffer: Vec<u8> = vec![];
let _ = file.take(MAX_PEEK_SIZE as u64).read_to_end(&mut buffer); let _ = file.take(MAX_PEEK_SIZE as u64).read_to_end(&mut buffer);
if content_inspector::inspect(&buffer).is_binary() { if content_inspector::inspect(&buffer).is_binary() {
ItemType::Binary gopher::Type::Binary
} else { } else {
ItemType::File gopher::Type::Text
} }
} else { } else {
ItemType::Error gopher::Type::Error
} }
} else if metadata.is_dir() { } else if metadata.is_dir() {
ItemType::Directory gopher::Type::Menu
} else { } else {
ItemType::Error gopher::Type::Error
} }
} }
@ -332,3 +398,128 @@ fn shell(path: &str, args: &[&str]) -> Result<String> {
Ok(str::from_utf8(&output.stderr)?.to_string()) Ok(str::from_utf8(&output.stderr)?.to_string())
} }
} }
/// Sort directory paths: dirs first, files 2nd, version #s respected.
fn sort_paths(dir_path: &str, reverse: bool) -> Result<Vec<DirEntry>> {
let mut paths: Vec<_> = fs::read_dir(dir_path)?.filter_map(|r| r.ok()).collect();
let is_dir = |entry: &fs::DirEntry| match entry.file_type() {
Ok(t) => t.is_dir(),
_ => false,
};
paths.sort_by(|a, b| {
let a_is_dir = is_dir(a);
let b_is_dir = is_dir(b);
if a_is_dir && b_is_dir || !a_is_dir && !b_is_dir {
let ord = alphanumeric_sort::compare_os_str::<&Path, &Path>(
a.path().as_ref(),
b.path().as_ref(),
);
if reverse {
ord.reverse()
} else {
ord
}
} else if is_dir(a) {
Ordering::Less
} else if is_dir(b) {
Ordering::Greater
} else {
Ordering::Equal // what
}
});
Ok(paths)
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! str_path {
($e:expr) => {
$e.path()
.to_str()
.unwrap()
.trim_start_matches("tests/sort/")
};
}
#[test]
fn test_sort_directory() {
let paths = sort_paths("tests/sort", false).unwrap();
assert_eq!(str_path!(paths[0]), "zzz");
assert_eq!(str_path!(paths[1]), "phetch-v0.1.7-linux-armv7.tar.gz");
assert_eq!(
str_path!(paths[paths.len() - 1]),
"phetch-v0.1.11-macos.zip"
);
}
#[test]
fn test_rsort_directory() {
let paths = sort_paths("tests/sort", true).unwrap();
assert_eq!(str_path!(paths[0]), "zzz");
assert_eq!(str_path!(paths[1]), "phetch-v0.1.11-macos.zip");
assert_eq!(
str_path!(paths[paths.len() - 1]),
"phetch-v0.1.7-linux-armv7.tar.gz"
);
}
#[test]
fn test_gph_line_to_gopher() {
let req = Request::from("localhost", 70, ".").unwrap();
assert_eq!(
gph_line_to_gopher("regular line test", &req),
"iregular line test (null) localhost 70\r\n"
);
assert_eq!(
gph_line_to_gopher("1link test /test localhost 70", &req),
"1link test /test localhost 70\r\n"
);
let line = "0short link test /test";
assert_eq!(
gph_line_to_gopher(line, &req),
"0short link test /test localhost 70\r\n"
);
}
#[test]
fn test_gph_geomyidae() {
let req = Request::from("localhost", 7070, ".").unwrap();
assert_eq!(
gph_line_to_gopher("[1|phkt.io|/|phkt.io]", &req),
"1phkt.io / phkt.io 70\r\n"
);
assert_eq!(gph_line_to_gopher("#[1|phkt.io|/|phkt.io]", &req), "");
assert_eq!(
gph_line_to_gopher("[1|sdf6000|/not-real|sdf.org|6000]", &req),
"1sdf6000 /not-real sdf.org 6000\r\n"
);
assert_eq!(
gph_line_to_gopher("[1|R-36|/]", &req),
"1R-36 / localhost 7070\r\n"
);
assert_eq!(
gph_line_to_gopher("[1|R-36|/|server|port]", &req),
"1R-36 / localhost 7070\r\n"
);
assert_eq!(
gph_line_to_gopher("[0|file - comment|/file.dat|server|port]", &req),
"0file - comment /file.dat localhost 7070\r\n"
);
assert_eq!(
gph_line_to_gopher(
"[0|some \\| escape and [ special characters ] test|error|server|port]",
&req
),
"0some \\| escape and [ special characters ] test error localhost 7070\r\n"
);
assert_eq!(
gph_line_to_gopher("[|empty type||server|port]", &req),
"empty type\t\tlocalhost\t7070\r\n",
);
}
}

View File

View File

View File

View File

View File

0
tests/sort/zzz/.gitignore vendored Normal file
View File