Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
013d93bacc | ||
|
|
0df8613789 | ||
|
|
68a1531a11 | ||
|
|
99c3be1804 | ||
|
|
80559e7224 | ||
|
|
c5c5f1ca44 | ||
|
|
de6a13a884 | ||
|
|
ef16355f9f | ||
|
|
a4dc0ed9b4 | ||
|
|
cd0e366979 | ||
|
|
d4c6c458f9 | ||
|
|
c9970b3a8e | ||
|
|
404d322b99 | ||
|
|
1b353d0b46 | ||
|
|
f920a5e4d6 | ||
|
|
d3ca1ecad3 | ||
|
|
9e057472c6 | ||
|
|
d453145bf8 | ||
|
|
8c131d649f | ||
|
|
a221fdb368 | ||
|
|
15dd82e300 | ||
|
|
de492caaa5 | ||
|
|
9096447c70 | ||
|
|
354340db86 | ||
|
|
900dd8d163 | ||
|
|
a11c4496b0 | ||
|
|
dd33b16876 | ||
|
|
2cc1870033 | ||
|
|
d41e6c041b | ||
|
|
460a461373 | ||
|
|
1e6e87b6aa | ||
|
|
54094270b3 | ||
|
|
e6cf367e23 | ||
|
|
e8437ecb28 | ||
|
|
543bebbd8d | ||
|
|
dc6c0200bc | ||
|
|
04bdb3072f | ||
|
|
a9228f0522 | ||
|
|
aae68c4c82 | ||
|
|
dd23826205 | ||
|
|
781f4cd3b5 | ||
|
|
6826b59ab9 | ||
|
|
2be725eeb5 | ||
|
|
dd2e9ca2e5 | ||
|
|
50bccae476 | ||
|
|
b3bcb1d85b | ||
|
|
c58d044459 | ||
|
|
eeaea0df16 | ||
|
|
2539aac4c0 | ||
|
|
03b9af543a | ||
|
|
1bb8141021 | ||
|
|
4bc8043f0f | ||
|
|
5effa38392 | ||
|
|
125aeeec3b | ||
|
|
c938ba6a2f | ||
|
|
f354affc36 | ||
|
|
7686b2ea64 | ||
|
|
b29b9a6a7c | ||
|
|
cbda57cfa8 | ||
|
|
b8aa545e8c | ||
|
|
22a031af5d | ||
|
|
6e6a60b305 | ||
|
|
77d6022d84 | ||
|
|
5db19d1a3e | ||
|
|
a6e891b3c5 | ||
|
|
d7a82a008b | ||
|
|
2369a4dd3c | ||
|
|
d27e53fb36 | ||
|
|
2cb51477d2 | ||
|
|
a308a20411 | ||
|
|
a6ddf1c13a | ||
|
|
8256d17efd | ||
|
|
efa12935ba | ||
|
|
7126a98023 | ||
|
|
c7ee3ec6e2 | ||
|
|
c4218031e2 | ||
|
|
6f918f6c1c | ||
|
|
6ecda080e8 | ||
|
|
2e86ee67a5 | ||
|
|
359616b901 | ||
|
|
ea2cdab330 | ||
|
|
4434823c46 | ||
|
|
e0a78ffc9d |
13
.github/workflows/build_gnu_linux.yml
vendored
13
.github/workflows/build_gnu_linux.yml
vendored
@@ -3,6 +3,17 @@ name: GNU/Linux
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'assets/'
|
||||
- 'dist/'
|
||||
- 'docs/'
|
||||
- 'snap/'
|
||||
- '.adr-dir'
|
||||
- 'Dockerfile'
|
||||
- 'LICENSE'
|
||||
- 'Makefile'
|
||||
- 'monolith.nuspec'
|
||||
- 'README.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -17,6 +28,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all --locked --verbose
|
||||
|
||||
13
.github/workflows/build_macos.yml
vendored
13
.github/workflows/build_macos.yml
vendored
@@ -3,6 +3,17 @@ name: macOS
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'assets/'
|
||||
- 'dist/'
|
||||
- 'docs/'
|
||||
- 'snap/'
|
||||
- '.adr-dir'
|
||||
- 'Dockerfile'
|
||||
- 'LICENSE'
|
||||
- 'Makefile'
|
||||
- 'monolith.nuspec'
|
||||
- 'README.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -17,6 +28,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all --locked --verbose
|
||||
|
||||
13
.github/workflows/build_windows.yml
vendored
13
.github/workflows/build_windows.yml
vendored
@@ -3,6 +3,17 @@ name: Windows
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'assets/'
|
||||
- 'dist/'
|
||||
- 'docs/'
|
||||
- 'snap/'
|
||||
- '.adr-dir'
|
||||
- 'Dockerfile'
|
||||
- 'LICENSE'
|
||||
- 'Makefile'
|
||||
- 'monolith.nuspec'
|
||||
- 'README.md'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -17,6 +28,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all --locked --verbose
|
||||
|
||||
57
.github/workflows/cd.yml
vendored
57
.github/workflows/cd.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# CD GitHub Actions workflow for Monolith
|
||||
# CD GitHub Actions workflow for monolith
|
||||
|
||||
name: CD
|
||||
|
||||
@@ -13,10 +13,13 @@ jobs:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build the executable
|
||||
run: cargo build --release
|
||||
|
||||
- uses: Shopify/upload-to-release@1.0.0
|
||||
with:
|
||||
name: monolith.exe
|
||||
@@ -28,24 +31,27 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare cross-platform environment
|
||||
run: |
|
||||
sudo mkdir -p /cross-build-arm
|
||||
sudo mkdir /cross-build
|
||||
sudo touch /etc/apt/sources.list.d/armhf.list
|
||||
echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ bionic main" | sudo tee -a /etc/apt/sources.list.d/armhf.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross
|
||||
sudo apt-get download libssl1.1:armhf libssl-dev:armhf
|
||||
sudo dpkg -x libssl1.1*.deb /cross-build-arm
|
||||
sudo dpkg -x libssl-dev*.deb /cross-build-arm
|
||||
sudo dpkg -x libssl1.1*.deb /cross-build
|
||||
sudo dpkg -x libssl-dev*.deb /cross-build
|
||||
rustup target add arm-unknown-linux-gnueabihf
|
||||
echo "C_INCLUDE_PATH=/cross-build-arm/usr/include" >> $GITHUB_ENV
|
||||
echo "OPENSSL_INCLUDE_DIR=/cross-build-arm/usr/include/arm-linux-gnueabihf" >> $GITHUB_ENV
|
||||
echo "OPENSSL_LIB_DIR=/cross-build-arm/usr/lib/arm-linux-gnueabihf" >> $GITHUB_ENV
|
||||
echo "C_INCLUDE_PATH=/cross-build/usr/include" >> $GITHUB_ENV
|
||||
echo "OPENSSL_INCLUDE_DIR=/cross-build/usr/include/arm-linux-gnueabihf" >> $GITHUB_ENV
|
||||
echo "OPENSSL_LIB_DIR=/cross-build/usr/lib/arm-linux-gnueabihf" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
|
||||
echo "RUSTFLAGS=-C linker=arm-linux-gnueabihf-gcc -L/usr/arm-linux-gnueabihf/lib -L/cross-build-arm/usr/lib/arm-linux-gnueabihf -L/cross-build-arm/lib/arm-linux-gnueabihf" >> $GITHUB_ENV
|
||||
echo "RUSTFLAGS=-C linker=arm-linux-gnueabihf-gcc -L/usr/arm-linux-gnueabihf/lib -L/cross-build/usr/lib/arm-linux-gnueabihf -L/cross-build/lib/arm-linux-gnueabihf" >> $GITHUB_ENV
|
||||
|
||||
- name: Build the executable
|
||||
run: cargo build --release --target=arm-unknown-linux-gnueabihf
|
||||
|
||||
- name: Attach artifact to the release
|
||||
uses: Shopify/upload-to-release@1.0.0
|
||||
with:
|
||||
@@ -53,13 +59,48 @@ jobs:
|
||||
path: target/arm-unknown-linux-gnueabihf/release/monolith
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
gnu_linux_aarch64:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Prepare cross-platform environment
|
||||
run: |
|
||||
sudo mkdir /cross-build
|
||||
sudo touch /etc/apt/sources.list.d/arm64.list
|
||||
echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main" | sudo tee -a /etc/apt/sources.list.d/arm64.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu libc6-arm64-cross libc6-dev-arm64-cross
|
||||
sudo apt-get download libssl1.1:arm64 libssl-dev:arm64
|
||||
sudo dpkg -x libssl1.1*.deb /cross-build
|
||||
sudo dpkg -x libssl-dev*.deb /cross-build
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
echo "C_INCLUDE_PATH=/cross-build/usr/include" >> $GITHUB_ENV
|
||||
echo "OPENSSL_INCLUDE_DIR=/cross-build/usr/include/aarch64-linux-gnu" >> $GITHUB_ENV
|
||||
echo "OPENSSL_LIB_DIR=/cross-build/usr/lib/aarch64-linux-gnu" >> $GITHUB_ENV
|
||||
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
|
||||
echo "RUSTFLAGS=-C linker=aarch64-linux-gnu-gcc -L/usr/aarch64-linux-gnu/lib -L/cross-build/usr/lib/aarch64-linux-gnu" >> $GITHUB_ENV
|
||||
|
||||
- name: Build the executable
|
||||
run: cargo build --release --target=aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Attach artifact to the release
|
||||
uses: Shopify/upload-to-release@1.0.0
|
||||
with:
|
||||
name: monolith-gnu-linux-aarch64
|
||||
path: target/aarch64-unknown-linux-gnu/release/monolith
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
gnu_linux_x86_64:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build the executable
|
||||
run: cargo build --release
|
||||
|
||||
- uses: Shopify/upload-to-release@1.0.0
|
||||
with:
|
||||
name: monolith-gnu-linux-x86_64
|
||||
|
||||
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -1,8 +1,21 @@
|
||||
# CI GitHub Actions workflow for monolith
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'assets/'
|
||||
- 'dist/'
|
||||
- 'docs/'
|
||||
- 'snap/'
|
||||
- '.adr-dir'
|
||||
- 'Dockerfile'
|
||||
- 'LICENSE'
|
||||
- 'Makefile'
|
||||
- 'monolith.nuspec'
|
||||
- 'README.md'
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
@@ -21,11 +34,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all --locked --verbose
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --all --locked --verbose
|
||||
|
||||
- name: Check code formatting
|
||||
run: |
|
||||
rustup component add rustfmt
|
||||
|
||||
1737
Cargo.lock
generated
1737
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "monolith"
|
||||
version = "2.4.1"
|
||||
version = "2.6.2"
|
||||
authors = [
|
||||
"Sunshine <sunshine@uberspace.net>",
|
||||
"Mahdi Robatipoor <mahdi.robatipoor@gmail.com>",
|
||||
@@ -22,21 +22,22 @@ include = [
|
||||
license = "CC0-1.0"
|
||||
|
||||
[dependencies]
|
||||
atty = "0.2" # Used for highlighting network errors
|
||||
base64 = "0.13.0"
|
||||
chrono = "0.4.19" # Used for formatting creation timestamp
|
||||
clap = "2.33.3"
|
||||
cssparser = "0.28.1"
|
||||
atty = "0.2.14" # Used for highlighting network errors
|
||||
base64 = "0.13.0" # Used for integrity attributes
|
||||
chrono = "0.4.20" # Used for formatting creation timestamp
|
||||
clap = "3.2.16"
|
||||
cssparser = "0.29.6"
|
||||
encoding_rs = "0.8.31"
|
||||
html5ever = "0.24.1"
|
||||
regex = "1.4.3" # Used for parsing srcset
|
||||
sha2 = "0.9.2" # Used for calculating checksums during integrity checks
|
||||
url = "2.2.0"
|
||||
percent-encoding = "2.1.0"
|
||||
regex = "1.6.0" # Used for parsing srcset and NOSCRIPT
|
||||
sha2 = "0.10.2" # Used for calculating checksums during integrity checks
|
||||
url = "2.2.2"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.11.0"
|
||||
version = "0.11.11"
|
||||
default-features = false
|
||||
features = ["default-tls", "blocking", "gzip"]
|
||||
features = ["default-tls", "blocking", "gzip", "brotli", "deflate"]
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "1.0.2"
|
||||
tempfile = "3.2.0"
|
||||
assert_cmd = "2.0.4"
|
||||
|
||||
24
Dockerfile
24
Dockerfile
@@ -1,18 +1,22 @@
|
||||
FROM rust
|
||||
|
||||
WORKDIR /usr/local/src/
|
||||
RUN curl -s https://api.github.com/repos/y2z/monolith/releases/latest \
|
||||
| grep "tarball_url.*\"," \
|
||||
| cut -d '"' -f 4 \
|
||||
| wget -qi - -O monolith.tar.gz
|
||||
FROM ekidd/rust-musl-builder as builder
|
||||
|
||||
RUN curl -L -o monolith.tar.gz $(curl -s https://api.github.com/repos/y2z/monolith/releases/latest \
|
||||
| grep "tarball_url.*\"," \
|
||||
| cut -d '"' -f 4)
|
||||
RUN tar xfz monolith.tar.gz \
|
||||
&& mv Y2Z-monolith-* monolith \
|
||||
&& rm monolith.tar.gz
|
||||
|
||||
WORKDIR /usr/local/src/monolith
|
||||
RUN ls -a
|
||||
WORKDIR monolith/
|
||||
RUN make install
|
||||
|
||||
|
||||
FROM alpine
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache openssl && \
|
||||
rm -rf "/var/cache/apk/*"
|
||||
|
||||
COPY --from=builder /home/rust/.cargo/bin/monolith /usr/bin/monolith
|
||||
WORKDIR /tmp
|
||||
CMD ["/usr/local/cargo/bin/monolith"]
|
||||
ENTRYPOINT ["/usr/bin/monolith"]
|
||||
|
||||
129
README.md
129
README.md
@@ -1,6 +1,6 @@
|
||||
[](https://github.com/Y2Z/monolith/actions?query=workflow%3AGNU%2FLinux)
|
||||
[](https://github.com/Y2Z/monolith/actions?query=workflow%3AmacOS)
|
||||
[](https://github.com/Y2Z/monolith/actions?query=workflow%3AWindows)
|
||||
[](https://github.com/Y2Z/monolith/actions?query=workflow%3AGNU%2FLinux)
|
||||
[](https://github.com/Y2Z/monolith/actions?query=workflow%3AmacOS)
|
||||
[](https://github.com/Y2Z/monolith/actions?query=workflow%3AWindows)
|
||||
|
||||
```
|
||||
_____ ______________ __________ ___________________ ___
|
||||
@@ -18,53 +18,101 @@ Unlike the conventional “Save page as”, `monolith` not only saves the target
|
||||
|
||||
If compared to saving websites with `wget -mpk`, this tool embeds all assets as data URLs and therefore lets browsers render the saved page exactly the way it was on the Internet, even when no network connection is available.
|
||||
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### Using Cargo
|
||||
$ cargo install monolith
|
||||
#### Using [Cargo](https://crates.io/crates/monolith)
|
||||
|
||||
#### Via Homebrew (on macOS and GNU/Linux)
|
||||
$ brew install monolith
|
||||
```console
|
||||
cargo install monolith
|
||||
```
|
||||
|
||||
#### Using Snapcraft (on GNU/Linux)
|
||||
$ snap install monolith
|
||||
#### Via [Homebrew](https://formulae.brew.sh/formula/monolith) (macOS and GNU/Linux)
|
||||
|
||||
#### Using Ports collection (on FreeBSD and TrueOS)
|
||||
$ cd /usr/ports/www/monolith/
|
||||
$ make install clean
|
||||
```console
|
||||
brew install monolith
|
||||
```
|
||||
|
||||
#### Using pre-built binaries (Windows, ARM-based devices, etc)
|
||||
Every [release](https://github.com/Y2Z/monolith/releases) contains pre-built binaries for Windows, GNU/Linux, as well as platforms with non-standart CPU architecture.
|
||||
#### Via [MacPorts](https://ports.macports.org/port/monolith/summary) (macOS)
|
||||
|
||||
#### From source
|
||||
```console
|
||||
sudo port install monolith
|
||||
```
|
||||
|
||||
Dependency: `libssl-dev`
|
||||
#### Using [Snapcraft](https://snapcraft.io/monolith) (GNU/Linux)
|
||||
|
||||
$ git clone https://github.com/Y2Z/monolith.git
|
||||
$ cd monolith
|
||||
$ make install
|
||||
```console
|
||||
snap install monolith
|
||||
```
|
||||
|
||||
#### Using Containers
|
||||
#### Using [FreeBSD packages](https://svnweb.freebsd.org/ports/head/www/monolith/) (FreeBSD)
|
||||
|
||||
```console
|
||||
pkg install monolith
|
||||
```
|
||||
|
||||
#### Using [FreeBSD ports](https://www.freshports.org/www/monolith/) (FreeBSD)
|
||||
|
||||
```console
|
||||
cd /usr/ports/www/monolith/
|
||||
make install clean
|
||||
```
|
||||
|
||||
#### Using [pkgsrc](https://pkgsrc.se/www/monolith) (NetBSD, OpenBSD, Haiku, etc)
|
||||
|
||||
```console
|
||||
cd /usr/pkgsrc/www/monolith
|
||||
make install clean
|
||||
```
|
||||
|
||||
#### Using [containers](https://www.docker.com/)
|
||||
|
||||
```console
|
||||
docker build -t Y2Z/monolith .
|
||||
sudo install -b dist/run-in-container.sh /usr/local/bin/monolith
|
||||
```
|
||||
|
||||
#### From [source](https://github.com/Y2Z/monolith)
|
||||
|
||||
Dependency: `libssl`
|
||||
|
||||
```console
|
||||
git clone https://github.com/Y2Z/monolith.git
|
||||
cd monolith
|
||||
make install
|
||||
```
|
||||
|
||||
#### Using [pre-built binaries](https://github.com/Y2Z/monolith/releases) (Windows, ARM-based devices, etc)
|
||||
|
||||
Every release contains pre-built binaries for Windows, GNU/Linux, as well as platforms with non-standard CPU architecture.
|
||||
|
||||
$ docker build -t Y2Z/monolith .
|
||||
$ sudo install -b utils/run-in-container.sh /usr/local/bin/monolith
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
## Usage
|
||||
$ monolith https://lyrics.github.io/db/P/Portishead/Dummy/Roads/ -o portishead-roads-lyrics.html
|
||||
or
|
||||
|
||||
$ cat index.html | monolith -aIiFfcMv - > index-processed.html
|
||||
```console
|
||||
monolith https://lyrics.github.io/db/P/Portishead/Dummy/Roads/ -o portishead-roads-lyrics.html
|
||||
```
|
||||
|
||||
```console
|
||||
cat index.html | monolith -aIiFfcMv -b https://original.site/ - > result.html
|
||||
```
|
||||
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
- `-a`: Exclude audio sources
|
||||
- `-b`: Use custom `base URL`
|
||||
- `-c`: Exclude CSS
|
||||
- `-C`: Save document using custom `charset`
|
||||
- `-e`: Ignore network errors
|
||||
- `-f`: Omit frames
|
||||
- `-F`: Exclude web fonts
|
||||
@@ -73,42 +121,53 @@ or
|
||||
- `-j`: Exclude JavaScript
|
||||
- `-k`: Accept invalid X.509 (TLS) certificates
|
||||
- `-M`: Don't add timestamp and URL information
|
||||
- `-o`: Write output to `file`
|
||||
- `-n`: Extract contents of NOSCRIPT elements
|
||||
- `-o`: Write output to `file` (use “-” for STDOUT)
|
||||
- `-s`: Be quiet
|
||||
- `-t`: Adjust `network request timeout`
|
||||
- `-u`: Provide `custom User-Agent`
|
||||
- `-u`: Provide custom `User-Agent`
|
||||
- `-v`: Exclude videos
|
||||
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
## Proxies
|
||||
|
||||
Please set `https_proxy`, `http_proxy`, and `no_proxy` environment variables.
|
||||
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Please open an issue if something is wrong, that helps make this project better.
|
||||
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
## Related projects
|
||||
- `Monolith Chrome Extension`: https://github.com/rhysd/monolith-of-web
|
||||
- `Pagesaver`: https://github.com/distributed-mind/pagesaver
|
||||
- `Personal WayBack Machine`: https://github.com/popey/pwbm
|
||||
- `Hako`: https://github.com/dmpop/hako
|
||||
|
||||
- Monolith Chrome Extension: https://github.com/rhysd/monolith-of-web
|
||||
- Pagesaver: https://github.com/distributed-mind/pagesaver
|
||||
- Personal WayBack Machine: https://github.com/popey/pwbm
|
||||
- Hako: https://github.com/dmpop/hako
|
||||
- Monk: https://github.com/monk-dev/monk
|
||||
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
## License
|
||||
|
||||
<a href="http://creativecommons.org/publicdomain/zero/1.0/">
|
||||
<img src="http://i.creativecommons.org/p/zero/1.0/88x31.png" alt="CC0-1.0" />
|
||||
</a>
|
||||
<br />
|
||||
To the extent possible under law, the author(s) have dedicated all copyright related and neighboring rights to this software to the public domain worldwide.
|
||||
This software is distributed without any warranty.
|
||||
|
||||
|
||||
---------------------------------------------------
|
||||
|
||||
|
||||
<!-- Microtext -->
|
||||
<sub>Keep in mind that `monolith` is not aware of your browser’s session</sub>
|
||||
|
||||
@@ -7,4 +7,4 @@ if which podman 2>&1 > /dev/null; then
|
||||
DOCKER=podman
|
||||
fi
|
||||
|
||||
$DOCKER run --rm Y2Z/$PROG_NAME $PROG_NAME "$@"
|
||||
$DOCKER run --rm Y2Z/$PROG_NAME "$@"
|
||||
@@ -1,15 +0,0 @@
|
||||
1. Run `docker build -t y2z/monolith .` to create a Docker image
|
||||
|
||||
2. Create a file named `monolith` which contains:
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
docker run --rm \
|
||||
y2z/monolith \
|
||||
monolith \
|
||||
"$@"
|
||||
```
|
||||
3. Make the file executable (`chmod +x monolith`) and include it into your `$PATH`
|
||||
4. Now you should be able to run a containerized build of monolith like this:
|
||||
`monolith -I https://github.com > document.html`
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
# Web apps that can be saved with Monolith
|
||||
|
||||
These apps retain most or all of their functionality when saved with Monolith
|
||||
These apps retain all or most of their functionality when saved with Monolith:
|
||||
|
||||
|Converse|https://conversejs.org|
|
||||
|:--|:--|
|
||||
|Description|An XMPP client built using web technologies|
|
||||
|Functionality retained|**full**|
|
||||
|Command to use|`monolith https://conversejs.org/fullscreen.html > conversejs.html`|
|
||||
|Monolith version used|2.2.7|
|
||||
|
||||
## Converse
|
||||
|
||||
| Website | https://conversejs.org |
|
||||
|:-----------------------|:--------------------------------------------------------------------|
|
||||
| Description | An XMPP client built using web technologies |
|
||||
| Functionality retained | **full** |
|
||||
| Command to use | `monolith https://conversejs.org/fullscreen.html > conversejs.html` |
|
||||
| Monolith version used | 2.2.7 |
|
||||
|
||||
|
||||
## Markdown Tables generator
|
||||
|
||||
| Website | https://www.tablesgenerator.com |
|
||||
|:--------------------------|:-----------------------------------------------------------------------------------------------|
|
||||
| Description | Tool for creating tables in extended Markdown format |
|
||||
| Functionality retained | **full** |
|
||||
| Command to use | `monolith -I https://www.tablesgenerator.com/markdown_tables -o markdown-table-generator.html` |
|
||||
| Monolith version used | 2.6.1 |
|
||||
|
||||
257
src/css.rs
257
src/css.rs
@@ -1,9 +1,12 @@
|
||||
use cssparser::{ParseError, Parser, ParserInput, SourcePosition, Token};
|
||||
use cssparser::{
|
||||
serialize_identifier, serialize_string, ParseError, Parser, ParserInput, SourcePosition, Token,
|
||||
};
|
||||
use reqwest::blocking::Client;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
use crate::opts::Options;
|
||||
use crate::url::{data_to_data_url, get_url_fragment, is_http_url, resolve_url, url_with_fragment};
|
||||
use crate::url::{create_data_url, resolve_url, EMPTY_IMAGE_DATA_URL};
|
||||
use crate::utils::retrieve_asset;
|
||||
|
||||
const CSS_PROPS_WITH_IMAGE_URLS: &[&str] = &[
|
||||
@@ -26,7 +29,44 @@ const CSS_PROPS_WITH_IMAGE_URLS: &[&str] = &[
|
||||
"suffix",
|
||||
"symbols",
|
||||
];
|
||||
const CSS_SPECIAL_CHARS: &'static str = "~!@$%^&*()+=,./'\";:?><[]{}|`#";
|
||||
|
||||
pub fn embed_css(
|
||||
cache: &mut HashMap<String, Vec<u8>>,
|
||||
client: &Client,
|
||||
document_url: &Url,
|
||||
css: &str,
|
||||
options: &Options,
|
||||
depth: u32,
|
||||
) -> String {
|
||||
let mut input = ParserInput::new(&css);
|
||||
let mut parser = Parser::new(&mut input);
|
||||
|
||||
process_css(
|
||||
cache,
|
||||
client,
|
||||
document_url,
|
||||
&mut parser,
|
||||
options,
|
||||
depth,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn format_ident(ident: &str) -> String {
|
||||
let mut res: String = "".to_string();
|
||||
let _ = serialize_identifier(ident, &mut res);
|
||||
res = res.trim_end().to_string();
|
||||
res
|
||||
}
|
||||
|
||||
pub fn format_quoted_string(string: &str) -> String {
|
||||
let mut res: String = "".to_string();
|
||||
let _ = serialize_string(string, &mut res);
|
||||
res
|
||||
}
|
||||
|
||||
pub fn is_image_url_prop(prop_name: &str) -> bool {
|
||||
CSS_PROPS_WITH_IMAGE_URLS
|
||||
@@ -35,30 +75,10 @@ pub fn is_image_url_prop(prop_name: &str) -> bool {
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn enquote(input: String, double: bool) -> String {
|
||||
if double {
|
||||
format!("\"{}\"", input.replace("\"", "\\\""))
|
||||
} else {
|
||||
format!("'{}'", input.replace("'", "\\'"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn escape(value: &str) -> String {
|
||||
let mut res = str!(&value);
|
||||
|
||||
res = res.replace("\\", "\\\\");
|
||||
|
||||
for c in CSS_SPECIAL_CHARS.chars() {
|
||||
res = res.replace(c, format!("\\{}", c).as_str());
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub fn process_css<'a>(
|
||||
cache: &mut HashMap<String, Vec<u8>>,
|
||||
client: &Client,
|
||||
parent_url: &str,
|
||||
document_url: &Url,
|
||||
parser: &mut Parser,
|
||||
options: &Options,
|
||||
depth: u32,
|
||||
@@ -66,10 +86,10 @@ pub fn process_css<'a>(
|
||||
prop_name: &str,
|
||||
func_name: &str,
|
||||
) -> Result<String, ParseError<'a, String>> {
|
||||
let mut result: String = str!();
|
||||
let mut result: String = "".to_string();
|
||||
|
||||
let mut curr_rule: String = str!(rule_name.clone());
|
||||
let mut curr_prop: String = str!(prop_name.clone());
|
||||
let mut curr_rule: String = rule_name.clone().to_string();
|
||||
let mut curr_prop: String = prop_name.clone().to_string();
|
||||
let mut token: &Token;
|
||||
let mut token_offset: SourcePosition;
|
||||
|
||||
@@ -85,7 +105,7 @@ pub fn process_css<'a>(
|
||||
match *token {
|
||||
Token::Comment(_) => {
|
||||
let token_slice = parser.slice_from(token_offset);
|
||||
result.push_str(str!(token_slice).as_str());
|
||||
result.push_str(token_slice);
|
||||
}
|
||||
Token::Semicolon => result.push_str(";"),
|
||||
Token::Colon => result.push_str(":"),
|
||||
@@ -112,7 +132,7 @@ pub fn process_css<'a>(
|
||||
process_css(
|
||||
cache,
|
||||
client,
|
||||
parent_url,
|
||||
document_url,
|
||||
parser,
|
||||
options,
|
||||
depth,
|
||||
@@ -141,13 +161,13 @@ pub fn process_css<'a>(
|
||||
}
|
||||
// div...
|
||||
Token::Ident(ref value) => {
|
||||
curr_rule = str!();
|
||||
curr_prop = str!(value);
|
||||
result.push_str(&escape(value));
|
||||
curr_rule = "".to_string();
|
||||
curr_prop = value.to_string();
|
||||
result.push_str(&format_ident(value));
|
||||
}
|
||||
// @import, @font-face, @charset, @media...
|
||||
Token::AtKeyword(ref value) => {
|
||||
curr_rule = str!(value);
|
||||
curr_rule = value.to_string();
|
||||
if options.no_fonts && curr_rule == "font-face" {
|
||||
continue;
|
||||
}
|
||||
@@ -161,27 +181,32 @@ pub fn process_css<'a>(
|
||||
Token::QuotedString(ref value) => {
|
||||
if curr_rule == "import" {
|
||||
// Reset current at-rule value
|
||||
curr_rule = str!();
|
||||
curr_rule = "".to_string();
|
||||
|
||||
// Skip empty import values
|
||||
if value.len() < 1 {
|
||||
if value.len() == 0 {
|
||||
result.push_str("''");
|
||||
continue;
|
||||
}
|
||||
|
||||
let import_full_url = resolve_url(&parent_url, value).unwrap_or_default();
|
||||
let import_url_fragment = get_url_fragment(import_full_url.clone());
|
||||
let import_full_url: Url = resolve_url(&document_url, value);
|
||||
match retrieve_asset(
|
||||
cache,
|
||||
client,
|
||||
&parent_url,
|
||||
&document_url,
|
||||
&import_full_url,
|
||||
options,
|
||||
depth + 1,
|
||||
) {
|
||||
Ok((import_contents, import_final_url, _import_media_type)) => {
|
||||
let import_data_url = data_to_data_url(
|
||||
"text/css",
|
||||
Ok((
|
||||
import_contents,
|
||||
import_final_url,
|
||||
import_media_type,
|
||||
import_charset,
|
||||
)) => {
|
||||
let mut import_data_url = create_data_url(
|
||||
&import_media_type,
|
||||
&import_charset,
|
||||
embed_css(
|
||||
cache,
|
||||
client,
|
||||
@@ -193,63 +218,64 @@ pub fn process_css<'a>(
|
||||
.as_bytes(),
|
||||
&import_final_url,
|
||||
);
|
||||
let assembled_url: String = url_with_fragment(
|
||||
import_data_url.as_str(),
|
||||
import_url_fragment.as_str(),
|
||||
import_data_url.set_fragment(import_full_url.fragment());
|
||||
result.push_str(
|
||||
format_quoted_string(&import_data_url.to_string()).as_str(),
|
||||
);
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
}
|
||||
Err(_) => {
|
||||
// Keep remote reference if unable to retrieve the asset
|
||||
if is_http_url(import_full_url.clone()) {
|
||||
let assembled_url: String = url_with_fragment(
|
||||
import_full_url.as_str(),
|
||||
import_url_fragment.as_str(),
|
||||
if import_full_url.scheme() == "http"
|
||||
|| import_full_url.scheme() == "https"
|
||||
{
|
||||
result.push_str(
|
||||
format_quoted_string(&import_full_url.to_string()).as_str(),
|
||||
);
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if func_name == "url" {
|
||||
// Skip empty url()'s
|
||||
if value.len() < 1 {
|
||||
if value.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if options.no_images && is_image_url_prop(curr_prop.as_str()) {
|
||||
result.push_str(enquote(str!(empty_image!()), false).as_str());
|
||||
result.push_str(format_quoted_string(EMPTY_IMAGE_DATA_URL).as_str());
|
||||
} else {
|
||||
let resolved_url = resolve_url(&parent_url, value).unwrap_or_default();
|
||||
let url_fragment = get_url_fragment(resolved_url.clone());
|
||||
let resolved_url: Url = resolve_url(&document_url, value);
|
||||
match retrieve_asset(
|
||||
cache,
|
||||
client,
|
||||
&parent_url,
|
||||
&document_url,
|
||||
&resolved_url,
|
||||
options,
|
||||
depth + 1,
|
||||
) {
|
||||
Ok((data, final_url, media_type)) => {
|
||||
let data_url = data_to_data_url(&media_type, &data, &final_url);
|
||||
let assembled_url: String =
|
||||
url_with_fragment(data_url.as_str(), url_fragment.as_str());
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
Ok((data, final_url, media_type, charset)) => {
|
||||
let mut data_url =
|
||||
create_data_url(&media_type, &charset, &data, &final_url);
|
||||
data_url.set_fragment(resolved_url.fragment());
|
||||
result.push_str(
|
||||
format_quoted_string(&data_url.to_string()).as_str(),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
// Keep remote reference if unable to retrieve the asset
|
||||
if is_http_url(resolved_url.clone()) {
|
||||
let assembled_url: String = url_with_fragment(
|
||||
resolved_url.as_str(),
|
||||
url_fragment.as_str(),
|
||||
if resolved_url.scheme() == "http"
|
||||
|| resolved_url.scheme() == "https"
|
||||
{
|
||||
result.push_str(
|
||||
format_quoted_string(&resolved_url.to_string())
|
||||
.as_str(),
|
||||
);
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.push_str(enquote(str!(value), false).as_str());
|
||||
result.push_str(format_quoted_string(value).as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,7 +297,7 @@ pub fn process_css<'a>(
|
||||
if *has_sign && *unit_value >= 0. {
|
||||
result.push_str("+");
|
||||
}
|
||||
result.push_str(str!(unit_value * 100.0).as_str());
|
||||
result.push_str(&(unit_value * 100.0).to_string());
|
||||
result.push_str("%");
|
||||
}
|
||||
Token::Dimension {
|
||||
@@ -283,21 +309,22 @@ pub fn process_css<'a>(
|
||||
if *has_sign && *value >= 0. {
|
||||
result.push_str("+");
|
||||
}
|
||||
result.push_str(str!(value).as_str());
|
||||
result.push_str(str!(unit).as_str());
|
||||
result.push_str(&value.to_string());
|
||||
result.push_str(&unit.to_string());
|
||||
}
|
||||
// #selector, #id...
|
||||
Token::IDHash(ref value) => {
|
||||
curr_rule = str!();
|
||||
curr_rule = "".to_string();
|
||||
result.push_str("#");
|
||||
result.push_str(&escape(value));
|
||||
result.push_str(&format_ident(value));
|
||||
}
|
||||
// url()
|
||||
Token::UnquotedUrl(ref value) => {
|
||||
let is_import: bool = curr_rule == "import";
|
||||
|
||||
if is_import {
|
||||
// Reset current at-rule value
|
||||
curr_rule = str!();
|
||||
curr_rule = "".to_string();
|
||||
}
|
||||
|
||||
// Skip empty url()'s
|
||||
@@ -313,13 +340,19 @@ pub fn process_css<'a>(
|
||||
|
||||
result.push_str("url(");
|
||||
if is_import {
|
||||
let full_url = resolve_url(&parent_url, value).unwrap_or_default();
|
||||
let url_fragment = get_url_fragment(full_url.clone());
|
||||
match retrieve_asset(cache, client, &parent_url, &full_url, options, depth + 1)
|
||||
{
|
||||
Ok((css, final_url, _media_type)) => {
|
||||
let data_url = data_to_data_url(
|
||||
"text/css",
|
||||
let full_url: Url = resolve_url(&document_url, value);
|
||||
match retrieve_asset(
|
||||
cache,
|
||||
client,
|
||||
&document_url,
|
||||
&full_url,
|
||||
options,
|
||||
depth + 1,
|
||||
) {
|
||||
Ok((css, final_url, media_type, charset)) => {
|
||||
let mut data_url = create_data_url(
|
||||
&media_type,
|
||||
&charset,
|
||||
embed_css(
|
||||
cache,
|
||||
client,
|
||||
@@ -331,45 +364,43 @@ pub fn process_css<'a>(
|
||||
.as_bytes(),
|
||||
&final_url,
|
||||
);
|
||||
let assembled_url: String =
|
||||
url_with_fragment(data_url.as_str(), url_fragment.as_str());
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
data_url.set_fragment(full_url.fragment());
|
||||
result.push_str(format_quoted_string(&data_url.to_string()).as_str());
|
||||
}
|
||||
Err(_) => {
|
||||
// Keep remote reference if unable to retrieve the asset
|
||||
if is_http_url(full_url.clone()) {
|
||||
let assembled_url: String =
|
||||
url_with_fragment(full_url.as_str(), url_fragment.as_str());
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
if full_url.scheme() == "http" || full_url.scheme() == "https" {
|
||||
result
|
||||
.push_str(format_quoted_string(&full_url.to_string()).as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if is_image_url_prop(curr_prop.as_str()) && options.no_images {
|
||||
result.push_str(enquote(str!(empty_image!()), false).as_str());
|
||||
result.push_str(format_quoted_string(EMPTY_IMAGE_DATA_URL).as_str());
|
||||
} else {
|
||||
let full_url = resolve_url(&parent_url, value).unwrap_or_default();
|
||||
let url_fragment = get_url_fragment(full_url.clone());
|
||||
let full_url: Url = resolve_url(&document_url, value);
|
||||
match retrieve_asset(
|
||||
cache,
|
||||
client,
|
||||
&parent_url,
|
||||
&document_url,
|
||||
&full_url,
|
||||
options,
|
||||
depth + 1,
|
||||
) {
|
||||
Ok((data, final_url, media_type)) => {
|
||||
let data_url = data_to_data_url(&media_type, &data, &final_url);
|
||||
let assembled_url: String =
|
||||
url_with_fragment(data_url.as_str(), url_fragment.as_str());
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
Ok((data, final_url, media_type, charset)) => {
|
||||
let mut data_url =
|
||||
create_data_url(&media_type, &charset, &data, &final_url);
|
||||
data_url.set_fragment(full_url.fragment());
|
||||
result
|
||||
.push_str(format_quoted_string(&data_url.to_string()).as_str());
|
||||
}
|
||||
Err(_) => {
|
||||
// Keep remote reference if unable to retrieve the asset
|
||||
if is_http_url(full_url.clone()) {
|
||||
let assembled_url: String =
|
||||
url_with_fragment(full_url.as_str(), url_fragment.as_str());
|
||||
result.push_str(enquote(assembled_url, false).as_str());
|
||||
if full_url.scheme() == "http" || full_url.scheme() == "https" {
|
||||
result.push_str(
|
||||
format_quoted_string(&full_url.to_string()).as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,6 +408,7 @@ pub fn process_css<'a>(
|
||||
}
|
||||
result.push_str(")");
|
||||
}
|
||||
// =
|
||||
Token::Delim(ref value) => result.push_str(&value.to_string()),
|
||||
Token::Function(ref name) => {
|
||||
let function_name: &str = &name.clone();
|
||||
@@ -388,7 +420,7 @@ pub fn process_css<'a>(
|
||||
process_css(
|
||||
cache,
|
||||
client,
|
||||
parent_url,
|
||||
document_url,
|
||||
parser,
|
||||
options,
|
||||
depth,
|
||||
@@ -413,28 +445,3 @@ pub fn process_css<'a>(
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn embed_css(
|
||||
cache: &mut HashMap<String, Vec<u8>>,
|
||||
client: &Client,
|
||||
parent_url: &str,
|
||||
css: &str,
|
||||
options: &Options,
|
||||
depth: u32,
|
||||
) -> String {
|
||||
let mut input = ParserInput::new(&css);
|
||||
let mut parser = Parser::new(&mut input);
|
||||
|
||||
process_css(
|
||||
cache,
|
||||
client,
|
||||
parent_url,
|
||||
&mut parser,
|
||||
options,
|
||||
depth,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
1278
src/html.rs
1278
src/html.rs
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,6 @@
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
pub mod css;
|
||||
pub mod html;
|
||||
pub mod js;
|
||||
pub mod opts;
|
||||
pub mod url;
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#[macro_export]
|
||||
macro_rules! str {
|
||||
() => {
|
||||
String::new()
|
||||
};
|
||||
($val: expr) => {
|
||||
ToString::to_string(&$val)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! empty_image {
|
||||
() => {
|
||||
"data:image/png;base64,\
|
||||
iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAQAAADY4iz3AAAAEUlEQVR42mNkwAkYR6UolgIACvgADsuK6xYAAAAASUVORK5CYII="
|
||||
};
|
||||
}
|
||||
346
src/main.rs
346
src/main.rs
@@ -1,25 +1,23 @@
|
||||
use encoding_rs::Encoding;
|
||||
use html5ever::rcdom::RcDom;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, prelude::*, Error, Write};
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
use monolith::html::{
|
||||
add_favicon, create_metadata_tag, get_base_url, has_favicon, html_to_dom, set_base_url,
|
||||
stringify_document, walk_and_embed_assets,
|
||||
add_favicon, create_metadata_tag, get_base_url, get_charset, has_favicon, html_to_dom,
|
||||
serialize_document, set_base_url, set_charset, walk_and_embed_assets,
|
||||
};
|
||||
use monolith::opts::Options;
|
||||
use monolith::url::{
|
||||
data_to_data_url, is_data_url, is_file_url, is_http_url, parse_data_url, resolve_url,
|
||||
};
|
||||
use monolith::url::{create_data_url, resolve_url};
|
||||
use monolith::utils::retrieve_asset;
|
||||
|
||||
mod macros;
|
||||
|
||||
enum Output {
|
||||
Stdout(io::Stdout),
|
||||
File(fs::File),
|
||||
@@ -34,82 +32,116 @@ impl Output {
|
||||
}
|
||||
}
|
||||
|
||||
fn writeln_str(&mut self, s: &str) -> Result<(), Error> {
|
||||
fn write(&mut self, bytes: &Vec<u8>) -> Result<(), Error> {
|
||||
match self {
|
||||
Output::Stdout(stdout) => {
|
||||
writeln!(stdout, "{}", s)?;
|
||||
stdout.write_all(bytes)?;
|
||||
// Ensure newline at end of output
|
||||
if bytes.last() != Some(&b"\n"[0]) {
|
||||
stdout.write(b"\n")?;
|
||||
}
|
||||
stdout.flush()
|
||||
}
|
||||
Output::File(f) => {
|
||||
writeln!(f, "{}", s)?;
|
||||
f.flush()
|
||||
Output::File(file) => {
|
||||
file.write_all(bytes)?;
|
||||
// Ensure newline at end of output
|
||||
if bytes.last() != Some(&b"\n"[0]) {
|
||||
file.write(b"\n")?;
|
||||
}
|
||||
file.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_stdin() -> String {
|
||||
let mut buffer = String::new();
|
||||
for line in io::stdin().lock().lines() {
|
||||
buffer += line.unwrap_or_default().as_str();
|
||||
buffer += "\n";
|
||||
pub fn read_stdin() -> Vec<u8> {
|
||||
let mut buffer: Vec<u8> = vec![];
|
||||
|
||||
match io::stdin().lock().read_to_end(&mut buffer) {
|
||||
Ok(_) => buffer,
|
||||
Err(_) => buffer,
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let options = Options::from_args();
|
||||
let original_target: &str = &options.target;
|
||||
let target_url: &str;
|
||||
let mut base_url: String;
|
||||
let mut dom;
|
||||
let mut use_stdin: bool = false;
|
||||
let mut target: String = options.target.clone();
|
||||
|
||||
// Pre-process the input
|
||||
let cwd_normalized: String =
|
||||
str!(env::current_dir().unwrap().to_str().unwrap()).replace("\\", "/");
|
||||
let path = Path::new(original_target);
|
||||
let mut target: String = str!(original_target.clone()).replace("\\", "/");
|
||||
let path_is_relative: bool = path.is_relative();
|
||||
|
||||
// Determine exact target URL
|
||||
if target.clone().len() == 0 {
|
||||
// Check if target was provided
|
||||
if target.len() == 0 {
|
||||
if !options.silent {
|
||||
eprintln!("No target specified");
|
||||
}
|
||||
process::exit(1);
|
||||
} else if target.clone() == "-" {
|
||||
// Read from pipe (stdin)
|
||||
use_stdin = true;
|
||||
// Default target URL to empty data URL; the user can control it via --base-url
|
||||
target_url = "data:text/html,"
|
||||
} else if is_http_url(target.clone()) || is_data_url(target.clone()) {
|
||||
target_url = target.as_str();
|
||||
} else if is_file_url(target.clone()) {
|
||||
target_url = target.as_str();
|
||||
} else if path.exists() {
|
||||
if !path.is_file() {
|
||||
if !options.silent {
|
||||
eprintln!("Local target is not a file: {}", original_target);
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
target.insert_str(0, if cfg!(windows) { "file:///" } else { "file://" });
|
||||
if path_is_relative {
|
||||
target.insert_str(if cfg!(windows) { 8 } else { 7 }, &cwd_normalized);
|
||||
target.insert_str(
|
||||
if cfg!(windows) { 8 } else { 7 } + &cwd_normalized.len(),
|
||||
"/",
|
||||
);
|
||||
}
|
||||
target_url = target.as_str();
|
||||
} else {
|
||||
target.insert_str(0, "http://");
|
||||
target_url = target.as_str();
|
||||
}
|
||||
|
||||
// Define output
|
||||
let mut output = Output::new(&options.output).expect("Could not prepare output");
|
||||
// Check if custom charset is valid
|
||||
if let Some(custom_charset) = options.charset.clone() {
|
||||
if !Encoding::for_label_no_replacement(custom_charset.as_bytes()).is_some() {
|
||||
eprintln!("Unknown encoding: {}", &custom_charset);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let target_url: Url;
|
||||
let mut use_stdin: bool = false;
|
||||
|
||||
// Determine exact target URL
|
||||
if target.clone() == "-" {
|
||||
// Read from pipe (stdin)
|
||||
use_stdin = true;
|
||||
// Set default target URL to an empty data URL; the user can set it via --base-url
|
||||
target_url = Url::parse("data:text/html,").unwrap();
|
||||
} else {
|
||||
match Url::parse(&target.clone()) {
|
||||
Ok(parsed_url) => {
|
||||
if parsed_url.scheme() == "data"
|
||||
|| parsed_url.scheme() == "file"
|
||||
|| (parsed_url.scheme() == "http" || parsed_url.scheme() == "https")
|
||||
{
|
||||
target_url = parsed_url;
|
||||
} else {
|
||||
if !options.silent {
|
||||
eprintln!("Unsupported target URL type: {}", &parsed_url.scheme());
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
// Failed to parse given base URL (perhaps it's a filesystem path?)
|
||||
let path: &Path = Path::new(&target);
|
||||
|
||||
if path.exists() {
|
||||
if path.is_file() {
|
||||
match Url::from_file_path(fs::canonicalize(&path).unwrap()) {
|
||||
Ok(file_url) => {
|
||||
target_url = file_url;
|
||||
}
|
||||
Err(_err) => {
|
||||
if !options.silent {
|
||||
eprintln!(
|
||||
"Could not generate file URL out of given path: {}",
|
||||
"err"
|
||||
);
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !options.silent {
|
||||
eprintln!("Local target is not a file: {}", &options.target);
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
} else {
|
||||
// Last chance, now we do what browsers do:
|
||||
// prepend "http://" and hope it points to a website
|
||||
target.insert_str(0, "http://");
|
||||
target_url = Url::parse(&target).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize client
|
||||
let mut cache = HashMap::new();
|
||||
@@ -120,31 +152,52 @@ fn main() {
|
||||
HeaderValue::from_str(&user_agent).expect("Invalid User-Agent header specified"),
|
||||
);
|
||||
}
|
||||
let timeout: u64 = if options.timeout > 0 {
|
||||
options.timeout
|
||||
let client = if options.timeout > 0 {
|
||||
Client::builder().timeout(Duration::from_secs(options.timeout))
|
||||
} else {
|
||||
std::u64::MAX / 4
|
||||
};
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(timeout))
|
||||
.danger_accept_invalid_certs(options.insecure)
|
||||
.default_headers(header_map)
|
||||
.build()
|
||||
.expect("Failed to initialize HTTP client");
|
||||
// No timeout is default
|
||||
Client::builder()
|
||||
}
|
||||
.danger_accept_invalid_certs(options.insecure)
|
||||
.default_headers(header_map)
|
||||
.build()
|
||||
.expect("Failed to initialize HTTP client");
|
||||
|
||||
// At this stage we assume that the base URL is the same as the target URL
|
||||
base_url = str!(target_url);
|
||||
// At first we assume that base URL is the same as target URL
|
||||
let mut base_url: Url = target_url.clone();
|
||||
|
||||
let data: Vec<u8>;
|
||||
let mut document_encoding: String = "".to_string();
|
||||
let mut dom: RcDom;
|
||||
|
||||
// Retrieve target document
|
||||
if use_stdin {
|
||||
dom = html_to_dom(&read_stdin());
|
||||
} else if is_file_url(target_url) || is_http_url(target_url) {
|
||||
match retrieve_asset(&mut cache, &client, target_url, target_url, &options, 0) {
|
||||
Ok((data, final_url, _media_type)) => {
|
||||
if options.base_url.clone().unwrap_or(str!()).is_empty() {
|
||||
base_url = final_url
|
||||
data = read_stdin();
|
||||
} else if target_url.scheme() == "file"
|
||||
|| (target_url.scheme() == "http" || target_url.scheme() == "https")
|
||||
|| target_url.scheme() == "data"
|
||||
{
|
||||
match retrieve_asset(&mut cache, &client, &target_url, &target_url, &options, 0) {
|
||||
Ok((retrieved_data, final_url, media_type, charset)) => {
|
||||
// Make sure the media type is text/html
|
||||
if !media_type.eq_ignore_ascii_case("text/html") {
|
||||
if !options.silent {
|
||||
eprintln!("Unsupported document media type");
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
dom = html_to_dom(&String::from_utf8_lossy(&data));
|
||||
|
||||
if options
|
||||
.base_url
|
||||
.clone()
|
||||
.unwrap_or("".to_string())
|
||||
.is_empty()
|
||||
{
|
||||
base_url = final_url;
|
||||
}
|
||||
|
||||
data = retrieved_data;
|
||||
document_encoding = charset;
|
||||
}
|
||||
Err(_) => {
|
||||
if !options.silent {
|
||||
@@ -153,79 +206,128 @@ fn main() {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else if is_data_url(target_url) {
|
||||
let (media_type, data): (String, Vec<u8>) = parse_data_url(target_url);
|
||||
if !media_type.eq_ignore_ascii_case("text/html") {
|
||||
if !options.silent {
|
||||
eprintln!("Unsupported data URL media type");
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
dom = html_to_dom(&String::from_utf8_lossy(&data));
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
// Use custom base URL if specified, read and use what's in the DOM otherwise
|
||||
if !options.base_url.clone().unwrap_or(str!()).is_empty() {
|
||||
if is_data_url(options.base_url.clone().unwrap()) {
|
||||
if !options.silent {
|
||||
eprintln!("Data URLs cannot be used as base URL");
|
||||
// Initial parse
|
||||
dom = html_to_dom(&data, document_encoding.clone());
|
||||
|
||||
// TODO: investigate if charset from filesystem/data URL/HTTP headers
|
||||
// has say over what's specified in HTML
|
||||
|
||||
// Attempt to determine document's charset
|
||||
if let Some(html_charset) = get_charset(&dom.document) {
|
||||
if !html_charset.is_empty() {
|
||||
// Check if the charset specified inside HTML is valid
|
||||
if let Some(encoding) = Encoding::for_label_no_replacement(html_charset.as_bytes()) {
|
||||
document_encoding = html_charset;
|
||||
dom = html_to_dom(&data, encoding.name().to_string());
|
||||
}
|
||||
process::exit(1);
|
||||
} else {
|
||||
base_url = options.base_url.clone().unwrap();
|
||||
}
|
||||
} else {
|
||||
if let Some(existing_base_url) = get_base_url(&dom.document) {
|
||||
base_url = resolve_url(target_url, existing_base_url).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Embed remote assets
|
||||
// Use custom base URL if specified, read and use what's in the DOM otherwise
|
||||
let custom_base_url: String = options.base_url.clone().unwrap_or("".to_string());
|
||||
if custom_base_url.is_empty() {
|
||||
// No custom base URL is specified
|
||||
// Try to see if document has BASE element
|
||||
if let Some(existing_base_url) = get_base_url(&dom.document) {
|
||||
base_url = resolve_url(&target_url, &existing_base_url);
|
||||
}
|
||||
} else {
|
||||
// Custom base URL provided
|
||||
match Url::parse(&custom_base_url) {
|
||||
Ok(parsed_url) => {
|
||||
if parsed_url.scheme() == "file" {
|
||||
// File base URLs can only work with
|
||||
// documents saved from filesystem
|
||||
if target_url.scheme() == "file" {
|
||||
base_url = parsed_url;
|
||||
}
|
||||
} else {
|
||||
base_url = parsed_url;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Failed to parse given base URL, perhaps it's a filesystem path?
|
||||
if target_url.scheme() == "file" {
|
||||
// Relative paths could work for documents saved from filesystem
|
||||
let path: &Path = Path::new(&custom_base_url);
|
||||
if path.exists() {
|
||||
match Url::from_file_path(fs::canonicalize(&path).unwrap()) {
|
||||
Ok(file_url) => {
|
||||
base_url = file_url;
|
||||
}
|
||||
Err(_) => {
|
||||
if !options.silent {
|
||||
eprintln!(
|
||||
"Could not map given path to base URL: {}",
|
||||
custom_base_url
|
||||
);
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse through the document and embed remote assets
|
||||
walk_and_embed_assets(&mut cache, &client, &base_url, &dom.document, &options, 0);
|
||||
|
||||
// Update or add new BASE tag to reroute network requests and hash-links in the final document
|
||||
// Update or add new BASE element to reroute network requests and hash-links
|
||||
if let Some(new_base_url) = options.base_url.clone() {
|
||||
dom = set_base_url(&dom.document, new_base_url);
|
||||
}
|
||||
|
||||
// Request and embed /favicon.ico (unless it's already linked in the document)
|
||||
if !options.no_images && is_http_url(target_url) && !has_favicon(&dom.document) {
|
||||
let favicon_ico_url: String = resolve_url(&base_url, "/favicon.ico").unwrap();
|
||||
if !options.no_images
|
||||
&& (target_url.scheme() == "http" || target_url.scheme() == "https")
|
||||
&& !has_favicon(&dom.document)
|
||||
{
|
||||
let favicon_ico_url: Url = resolve_url(&base_url, "/favicon.ico");
|
||||
|
||||
match retrieve_asset(
|
||||
&mut cache,
|
||||
&client,
|
||||
&base_url,
|
||||
&target_url,
|
||||
&favicon_ico_url,
|
||||
&options,
|
||||
0,
|
||||
) {
|
||||
Ok((data, final_url, media_type)) => {
|
||||
let favicon_data_url: String = data_to_data_url(&media_type, &data, &final_url);
|
||||
dom = add_favicon(&dom.document, favicon_data_url);
|
||||
Ok((data, final_url, media_type, charset)) => {
|
||||
let favicon_data_url: Url =
|
||||
create_data_url(&media_type, &charset, &data, &final_url);
|
||||
dom = add_favicon(&dom.document, favicon_data_url.to_string());
|
||||
}
|
||||
Err(_) => {
|
||||
// Failed to retrieve favicon.ico
|
||||
// Failed to retrieve /favicon.ico
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save using specified charset, if given
|
||||
if let Some(custom_charset) = options.charset.clone() {
|
||||
document_encoding = custom_charset;
|
||||
dom = set_charset(dom, document_encoding.clone());
|
||||
}
|
||||
|
||||
// Serialize DOM tree
|
||||
let mut result: String = stringify_document(&dom.document, &options);
|
||||
let mut result: Vec<u8> = serialize_document(dom, document_encoding, &options);
|
||||
|
||||
// Add metadata tag
|
||||
// Prepend metadata comment tag
|
||||
if !options.no_metadata {
|
||||
let metadata_comment: String = create_metadata_tag(&target_url);
|
||||
result.insert_str(0, &metadata_comment);
|
||||
if metadata_comment.len() > 0 {
|
||||
result.insert_str(metadata_comment.len(), "\n");
|
||||
}
|
||||
let mut metadata_comment: String = create_metadata_tag(&target_url);
|
||||
metadata_comment += "\n";
|
||||
result.splice(0..0, metadata_comment.as_bytes().to_vec());
|
||||
}
|
||||
|
||||
// Define output
|
||||
let mut output = Output::new(&options.output).expect("Could not prepare output");
|
||||
|
||||
// Write result into stdout or file
|
||||
output
|
||||
.writeln_str(&result)
|
||||
.expect("Could not write HTML output");
|
||||
output.write(&result).expect("Could not write HTML output");
|
||||
}
|
||||
|
||||
26
src/opts.rs
26
src/opts.rs
@@ -6,6 +6,7 @@ pub struct Options {
|
||||
pub no_audio: bool,
|
||||
pub base_url: Option<String>,
|
||||
pub no_css: bool,
|
||||
pub charset: Option<String>,
|
||||
pub ignore_errors: bool,
|
||||
pub no_frames: bool,
|
||||
pub no_fonts: bool,
|
||||
@@ -21,6 +22,7 @@ pub struct Options {
|
||||
pub no_video: bool,
|
||||
pub target: String,
|
||||
pub no_color: bool,
|
||||
pub unwrap_noscript: bool,
|
||||
}
|
||||
|
||||
const ASCII: &'static str = " \
|
||||
@@ -41,12 +43,13 @@ const ENV_VAR_TERM: &str = "TERM";
|
||||
impl Options {
|
||||
pub fn from_args() -> Options {
|
||||
let app = App::new(env!("CARGO_PKG_NAME"))
|
||||
.version(crate_version!())
|
||||
.author(format!("\n{}", crate_authors!("\n")).as_str())
|
||||
.about(format!("{}\n{}", ASCII, crate_description!()).as_str())
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.author(format!("\n{}\n\n", env!("CARGO_PKG_AUTHORS").replace(':', "\n")).as_str())
|
||||
.about(format!("{}\n{}", ASCII, env!("CARGO_PKG_DESCRIPTION")).as_str())
|
||||
.args_from_usage("-a, --no-audio 'Removes audio sources'")
|
||||
.args_from_usage("-b, --base-url=[http://localhost/] 'Sets custom base URL'")
|
||||
.args_from_usage("-c, --no-css 'Removes CSS'")
|
||||
.args_from_usage("-C, --charset=[UTF-8] 'Enforces custom encoding'")
|
||||
.args_from_usage("-e, --ignore-errors 'Ignore network errors'")
|
||||
.args_from_usage("-f, --no-frames 'Removes frames and iframes'")
|
||||
.args_from_usage("-F, --no-fonts 'Removes fonts'")
|
||||
@@ -55,7 +58,12 @@ impl Options {
|
||||
.args_from_usage("-j, --no-js 'Removes JavaScript'")
|
||||
.args_from_usage("-k, --insecure 'Allows invalid X.509 (TLS) certificates'")
|
||||
.args_from_usage("-M, --no-metadata 'Excludes timestamp and source information'")
|
||||
.args_from_usage("-o, --output=[document.html] 'Writes output to <file>'")
|
||||
.args_from_usage(
|
||||
"-n, --unwrap-noscript 'Replaces NOSCRIPT elements with their contents'",
|
||||
)
|
||||
.args_from_usage(
|
||||
"-o, --output=[document.html] 'Writes output to <file>, use - for STDOUT'",
|
||||
)
|
||||
.args_from_usage("-s, --silent 'Suppresses verbosity'")
|
||||
.args_from_usage("-t, --timeout=[60] 'Adjusts network request timeout'")
|
||||
.args_from_usage("-u, --user-agent=[Firefox] 'Sets custom User-Agent string'")
|
||||
@@ -65,7 +73,7 @@ impl Options {
|
||||
.required(true)
|
||||
.takes_value(true)
|
||||
.index(1)
|
||||
.help("URL or file path, use - for stdin"),
|
||||
.help("URL or file path, use - for STDIN"),
|
||||
)
|
||||
.get_matches();
|
||||
let mut options: Options = Options::default();
|
||||
@@ -77,9 +85,12 @@ impl Options {
|
||||
.to_string();
|
||||
options.no_audio = app.is_present("no-audio");
|
||||
if let Some(base_url) = app.value_of("base-url") {
|
||||
options.base_url = Some(str!(base_url));
|
||||
options.base_url = Some(base_url.to_string());
|
||||
}
|
||||
options.no_css = app.is_present("no-css");
|
||||
if let Some(charset) = app.value_of("charset") {
|
||||
options.charset = Some(charset.to_string());
|
||||
}
|
||||
options.ignore_errors = app.is_present("ignore-errors");
|
||||
options.no_frames = app.is_present("no-frames");
|
||||
options.no_fonts = app.is_present("no-fonts");
|
||||
@@ -96,10 +107,11 @@ impl Options {
|
||||
.parse::<u64>()
|
||||
.unwrap();
|
||||
if let Some(user_agent) = app.value_of("user-agent") {
|
||||
options.user_agent = Some(str!(user_agent));
|
||||
options.user_agent = Some(user_agent.to_string());
|
||||
} else {
|
||||
options.user_agent = Some(DEFAULT_USER_AGENT.to_string());
|
||||
}
|
||||
options.unwrap_noscript = app.is_present("unwrap-noscript");
|
||||
options.no_video = app.is_present("no-video");
|
||||
|
||||
options.no_color =
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use assert_cmd::prelude::*;
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn local_file_target_input() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let cwd_normalized: String =
|
||||
str!(env::current_dir().unwrap().to_str().unwrap()).replace("\\", "/");
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg(if cfg!(windows) {
|
||||
"src\\tests\\data\\basic\\local-file.html"
|
||||
} else {
|
||||
"src/tests/data/basic/local-file.html"
|
||||
})
|
||||
.output()
|
||||
.unwrap();
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// STDOUT should contain HTML from the local file
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
"\
|
||||
<!DOCTYPE html><html lang=\"en\"><head>\n \
|
||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n \
|
||||
<title>Local HTML file</title>\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\" href=\"data:text/css;base64,Ym9keSB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogICAgY29sb3I6ICNmZmY7Cn0K\">\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n \
|
||||
<img alt=\"\">\n \
|
||||
<a href=\"file://local-file.html/\">Tricky href</a>\n \
|
||||
<a href=\"https://github.com/Y2Z/monolith\">Remote URL</a>\n \
|
||||
<script src=\"data:application/javascript;base64,ZG9jdW1lbnQuYm9keS5zdHlsZS5iYWNrZ3JvdW5kQ29sb3IgPSAiZ3JlZW4iOwpkb2N1bWVudC5ib2R5LnN0eWxlLmNvbG9yID0gInJlZCI7Cg==\"></script>\n\n\n\n\
|
||||
</body></html>\n\
|
||||
"
|
||||
);
|
||||
|
||||
// STDERR should contain list of retrieved file URLs
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
format!(
|
||||
"\
|
||||
{file}{cwd}/src/tests/data/basic/local-file.html\n \
|
||||
{file}{cwd}/src/tests/data/basic/local-style.css\n \
|
||||
{file}{cwd}/src/tests/data/basic/local-script.js\n\
|
||||
",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized
|
||||
)
|
||||
);
|
||||
|
||||
// The exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_file_target_input_absolute_target_path() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let cwd_normalized: String = str!(cwd.to_str().unwrap()).replace("\\", "/");
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-jciI")
|
||||
.arg(if cfg!(windows) {
|
||||
format!(
|
||||
"{cwd}\\src\\tests\\data\\basic\\local-file.html",
|
||||
cwd = cwd.to_str().unwrap()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{cwd}/src/tests/data/basic/local-file.html",
|
||||
cwd = cwd.to_str().unwrap()
|
||||
)
|
||||
})
|
||||
.output()
|
||||
.unwrap();
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// STDOUT should contain HTML from the local file
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
format!(
|
||||
"\
|
||||
<!DOCTYPE html><html lang=\"en\"><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-inline' data:; style-src 'none'; script-src 'none'; img-src data:;\"></meta>\n \
|
||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n \
|
||||
<title>Local HTML file</title>\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n \
|
||||
<img src=\"{empty_image}\" alt=\"\">\n \
|
||||
<a href=\"file://local-file.html/\">Tricky href</a>\n \
|
||||
<a href=\"https://github.com/Y2Z/monolith\">Remote URL</a>\n \
|
||||
<script></script>\n\n\n\n\
|
||||
</body></html>\n\
|
||||
",
|
||||
empty_image = empty_image!()
|
||||
)
|
||||
);
|
||||
|
||||
// STDERR should contain only the target file
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-file.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
);
|
||||
|
||||
// The exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_file_url_target_input() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let cwd_normalized: String =
|
||||
str!(env::current_dir().unwrap().to_str().unwrap()).replace("\\", "/");
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-cji")
|
||||
.arg(if cfg!(windows) {
|
||||
format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-file.html",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-file.html",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
})
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDOUT should contain HTML from the local file
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
format!(
|
||||
"\
|
||||
<!DOCTYPE html><html lang=\"en\"><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"style-src 'none'; script-src 'none'; img-src data:;\"></meta>\n \
|
||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n \
|
||||
<title>Local HTML file</title>\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n \
|
||||
<img src=\"{empty_image}\" alt=\"\">\n \
|
||||
<a href=\"file://local-file.html/\">Tricky href</a>\n \
|
||||
<a href=\"https://github.com/Y2Z/monolith\">Remote URL</a>\n \
|
||||
<script></script>\n\n\n\n\
|
||||
</body></html>\n\
|
||||
",
|
||||
empty_image = empty_image!()
|
||||
)
|
||||
);
|
||||
|
||||
// STDERR should contain list of retrieved file URLs
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
if cfg!(windows) {
|
||||
format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-file.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-file.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
// The exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embed_file_url_local_asset_within_style_attribute() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let file_url_prefix: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let mut file_svg = NamedTempFile::new()?;
|
||||
writeln!(file_svg, "<svg version=\"1.1\" baseProfile=\"full\" width=\"300\" height=\"200\" xmlns=\"http://www.w3.org/2000/svg\">\
|
||||
<rect width=\"100%\" height=\"100%\" fill=\"red\" />\
|
||||
<circle cx=\"150\" cy=\"100\" r=\"80\" fill=\"green\" />\
|
||||
<text x=\"150\" y=\"125\" font-size=\"60\" text-anchor=\"middle\" fill=\"white\">SVG</text>\
|
||||
</svg>\n")?;
|
||||
let mut file_html = NamedTempFile::new()?;
|
||||
writeln!(
|
||||
file_html,
|
||||
"<div style='background-image: url(\"{file}{path}\")'></div>\n",
|
||||
file = file_url_prefix,
|
||||
path = str!(file_svg.path().to_str().unwrap()).replace("\\", "/"),
|
||||
)?;
|
||||
let out = cmd.arg("-M").arg(file_html.path()).output().unwrap();
|
||||
|
||||
// STDOUT should contain HTML with date URL for background-image in it
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
"<html><head></head><body><div style=\"background-image: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGJhc2VQcm9maWxlPSJmdWxsIiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJyZWQiIC8+PGNpcmNsZSBjeD0iMTUwIiBjeT0iMTAwIiByPSI4MCIgZmlsbD0iZ3JlZW4iIC8+PHRleHQgeD0iMTUwIiB5PSIxMjUiIGZvbnQtc2l6ZT0iNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIj5TVkc8L3RleHQ+PC9zdmc+Cgo=')\"></div>\n\n</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should list temporary files that got retrieved
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
format!(
|
||||
"\
|
||||
{file}{html_path}\n \
|
||||
{file}{svg_path}\n\
|
||||
",
|
||||
file = file_url_prefix,
|
||||
html_path = str!(file_html.path().to_str().unwrap()).replace("\\", "/"),
|
||||
svg_path = str!(file_svg.path().to_str().unwrap()).replace("\\", "/"),
|
||||
)
|
||||
);
|
||||
|
||||
// The exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use assert_cmd::prelude::*;
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn change_encoding_to_utf_8() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let cwd_normalized: String = str!(cwd.to_str().unwrap()).replace("\\", "/");
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg(if cfg!(windows) {
|
||||
"src\\tests\\data\\unusual_encodings\\iso-8859-1.html"
|
||||
} else {
|
||||
"src/tests/data/unusual_encodings/iso-8859-1.html"
|
||||
})
|
||||
.output()
|
||||
.unwrap();
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// STDOUT should contain newly added base URL
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
"<html><head>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n </head>\n <body>\n © Some Company\n \n\n</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should contain only the target file
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
format!(
|
||||
"{file}{cwd}/src/tests/data/unusual_encodings/iso-8859-1.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
);
|
||||
|
||||
// The exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::css;
|
||||
|
||||
#[test]
|
||||
fn empty_input_single_quotes() {
|
||||
assert_eq!(css::enquote(str!(""), false), "''");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input_double_quotes() {
|
||||
assert_eq!(css::enquote(str!(""), true), "\"\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apostrophes_single_quotes() {
|
||||
assert_eq!(
|
||||
css::enquote(str!("It's a lovely day, don't you think?"), false),
|
||||
"'It\\'s a lovely day, don\\'t you think?'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apostrophes_double_quotes() {
|
||||
assert_eq!(
|
||||
css::enquote(str!("It's a lovely day, don't you think?"), true),
|
||||
"\"It's a lovely day, don't you think?\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feet_and_inches_single_quotes() {
|
||||
assert_eq!(
|
||||
css::enquote(str!("5'2\", 6'5\""), false),
|
||||
"'5\\'2\", 6\\'5\"'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feet_and_inches_double_quotes() {
|
||||
assert_eq!(
|
||||
css::enquote(str!("5'2\", 6'5\""), true),
|
||||
"\"5'2\\\", 6'5\\\"\""
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn encode_string_with_specific_media_type() {
|
||||
let mime = "application/javascript";
|
||||
let data = "var word = 'hello';\nalert(word);\n";
|
||||
let data_url = url::data_to_data_url(mime, data.as_bytes(), "");
|
||||
|
||||
assert_eq!(
|
||||
&data_url,
|
||||
"data:application/javascript;base64,dmFyIHdvcmQgPSAnaGVsbG8nOwphbGVydCh3b3JkKTsK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_append_fragment() {
|
||||
let data = "<svg></svg>\n";
|
||||
let data_url = url::data_to_data_url("image/svg+xml", data.as_bytes(), "");
|
||||
|
||||
assert_eq!(&data_url, "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4K");
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn decode_unicode_characters() {
|
||||
assert_eq!(
|
||||
url::decode_url(str!(
|
||||
"%E6%A4%9C%E3%83%92%E3%83%A0%E8%A7%A3%E5%A1%97%E3%82%83%E3%83%83%20%3D%20%E3%82%B5"
|
||||
)),
|
||||
"検ヒム解塗ゃッ = サ"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_file_url() {
|
||||
assert_eq!(
|
||||
url::decode_url(str!("file:///tmp/space%20here/test%231.html")),
|
||||
"file:///tmp/space here/test#1.html"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plus_sign() {
|
||||
assert_eq!(
|
||||
url::decode_url(str!(
|
||||
"fonts.somewhere.com/css?family=Open+Sans:300,400,400italic,600,600italic"
|
||||
)),
|
||||
"fonts.somewhere.com/css?family=Open+Sans:300,400,400italic,600,600italic"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn remove_protocl_and_fragment() {
|
||||
if cfg!(windows) {
|
||||
assert_eq!(
|
||||
url::file_url_to_fs_path("file:///C:/documents/some-path/some-file.svg#fragment"),
|
||||
"C:\\documents\\some-path\\some-file.svg"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
url::file_url_to_fs_path("file:///tmp/some-path/some-file.svg#fragment"),
|
||||
"/tmp/some-path/some-file.svg"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_urls() {
|
||||
if cfg!(windows) {
|
||||
assert_eq!(
|
||||
url::file_url_to_fs_path("file:///C:/Documents%20and%20Settings/some-file.html"),
|
||||
"C:\\Documents and Settings\\some-file.html"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
url::file_url_to_fs_path("file:///home/user/My%20Documents"),
|
||||
"/home/user/My Documents"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn data_url() {
|
||||
assert_eq!(
|
||||
url::get_url_fragment(
|
||||
"data:image/svg+xml;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h#test"
|
||||
),
|
||||
"test"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗
|
||||
// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝
|
||||
// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn https_empty() {
|
||||
assert_eq!(url::get_url_fragment("https://kernel.org#"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_fragment() {
|
||||
assert_eq!(url::get_url_fragment("https://kernel.org"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dummy_data_url() {
|
||||
assert_eq!(url::get_url_fragment("data:text/html,"), "");
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn data_url_text_html() {
|
||||
assert!(url::is_data_url(
|
||||
"data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_url_no_media_type() {
|
||||
assert!(url::is_data_url(
|
||||
"data:;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗
|
||||
// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝
|
||||
// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn https_url() {
|
||||
assert!(!url::is_data_url("https://kernel.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_protocol_url() {
|
||||
assert!(!url::is_data_url("//kernel.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string() {
|
||||
assert!(!url::is_data_url(""));
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn unix_file_url() {
|
||||
assert!(url::is_file_url(
|
||||
"file:///home/user/Websites/my-website/index.html"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_file_url() {
|
||||
assert!(url::is_file_url(
|
||||
"file:///C:/Documents%20and%20Settings/user/Websites/my-website/assets/images/logo.png"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unix_url_with_backslashes() {
|
||||
assert!(url::is_file_url(
|
||||
"file:\\\\\\home\\user\\Websites\\my-website\\index.html"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_file_url_with_backslashes() {
|
||||
assert!(url::is_file_url(
|
||||
"file:\\\\\\C:\\Documents%20and%20Settings\\user\\Websites\\my-website\\assets\\images\\logo.png"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗
|
||||
// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝
|
||||
// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn url_with_no_protocl() {
|
||||
assert!(!url::is_file_url("//kernel.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_slash_filename() {
|
||||
assert!(!url::is_file_url("./index.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_filename() {
|
||||
assert!(!url::is_file_url("some-local-page.htm"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_ip_port_url() {
|
||||
assert!(!url::is_file_url("https://1.2.3.4:80/www/index.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_url() {
|
||||
assert!(!url::is_file_url(
|
||||
"data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_word_file() {
|
||||
assert!(!url::is_file_url("file"));
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn http_url() {
|
||||
assert!(url::is_http_url("http://kernel.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_url() {
|
||||
assert!(url::is_http_url("https://www.rust-lang.org/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_url_with_backslashes() {
|
||||
assert!(url::is_http_url("http:\\\\freebsd.org\\"));
|
||||
}
|
||||
}
|
||||
|
||||
// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗
|
||||
// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝
|
||||
// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn url_with_no_protocol() {
|
||||
assert!(!url::is_http_url("//kernel.org"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_slash_filename() {
|
||||
assert!(!url::is_http_url("./index.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_filename() {
|
||||
assert!(!url::is_http_url("some-local-page.htm"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_ip_port_url() {
|
||||
assert!(!url::is_http_url("ftp://1.2.3.4/www/index.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_url() {
|
||||
assert!(!url::is_http_url(
|
||||
"data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
mod clean_url;
|
||||
mod data_to_data_url;
|
||||
mod decode_url;
|
||||
mod file_url_to_fs_path;
|
||||
mod get_url_fragment;
|
||||
mod is_data_url;
|
||||
mod is_file_url;
|
||||
mod is_http_url;
|
||||
mod parse_data_url;
|
||||
mod resolve_url;
|
||||
mod url_has_protocol;
|
||||
mod url_with_fragment;
|
||||
@@ -1,40 +0,0 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
|
||||
#[test]
|
||||
fn url_with_fragment_url() {
|
||||
let url = "https://localhost.localdomain/path/";
|
||||
let fragment = "test";
|
||||
let assembled_url = url::url_with_fragment(url, fragment);
|
||||
|
||||
assert_eq!(&assembled_url, "https://localhost.localdomain/path/#test");
|
||||
}
|
||||
#[test]
|
||||
fn url_with_fragment_empty_url() {
|
||||
let url = "https://localhost.localdomain/path/";
|
||||
let fragment = "";
|
||||
let assembled_url = url::url_with_fragment(url, fragment);
|
||||
|
||||
assert_eq!(&assembled_url, "https://localhost.localdomain/path/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_with_fragment_data_url() {
|
||||
let url = "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4K";
|
||||
let fragment = "fragment";
|
||||
let assembled_url = url::url_with_fragment(url, fragment);
|
||||
|
||||
assert_eq!(
|
||||
&assembled_url,
|
||||
"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4K#fragment"
|
||||
);
|
||||
}
|
||||
}
|
||||
187
src/url.rs
187
src/url.rs
@@ -1,167 +1,82 @@
|
||||
use base64;
|
||||
use url::{form_urlencoded, ParseError, Url};
|
||||
use percent_encoding::percent_decode_str;
|
||||
use url::Url;
|
||||
|
||||
use crate::utils::detect_media_type;
|
||||
use crate::utils::{detect_media_type, parse_content_type};
|
||||
|
||||
pub fn clean_url<T: AsRef<str>>(input: T) -> String {
|
||||
let mut url = Url::parse(input.as_ref()).unwrap();
|
||||
pub const EMPTY_IMAGE_DATA_URL: &'static str = "data:image/png;base64,\
|
||||
iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAQAAADY4iz3AAAAEUlEQVR42mNkwAkYR6UolgIACvgADsuK6xYAAAAASUVORK5CYII=";
|
||||
|
||||
// Clear fragment
|
||||
pub fn clean_url(url: Url) -> Url {
|
||||
let mut url = url.clone();
|
||||
|
||||
// Clear fragment (if any)
|
||||
url.set_fragment(None);
|
||||
|
||||
// Get rid of stray question mark
|
||||
if url.query() == Some("") {
|
||||
url.set_query(None);
|
||||
}
|
||||
|
||||
// Remove empty trailing ampersand(s)
|
||||
let mut result: String = url.to_string();
|
||||
while result.ends_with("&") {
|
||||
result.pop();
|
||||
}
|
||||
|
||||
result
|
||||
url
|
||||
}
|
||||
|
||||
pub fn data_to_data_url(media_type: &str, data: &[u8], url: &str) -> String {
|
||||
pub fn create_data_url(media_type: &str, charset: &str, data: &[u8], final_asset_url: &Url) -> Url {
|
||||
// TODO: move this block out of this function
|
||||
let media_type: String = if media_type.is_empty() {
|
||||
detect_media_type(data, &url)
|
||||
detect_media_type(data, &final_asset_url)
|
||||
} else {
|
||||
media_type.to_string()
|
||||
};
|
||||
|
||||
format!("data:{};base64,{}", media_type, base64::encode(data))
|
||||
let mut data_url: Url = Url::parse("data:,").unwrap();
|
||||
|
||||
let c: String =
|
||||
if !charset.trim().is_empty() && !charset.trim().eq_ignore_ascii_case("US-ASCII") {
|
||||
format!(";charset={}", charset.trim())
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
data_url.set_path(format!("{}{};base64,{}", media_type, c, base64::encode(data)).as_str());
|
||||
|
||||
data_url
|
||||
}
|
||||
|
||||
pub fn decode_url(input: String) -> String {
|
||||
let input: String = input.replace("+", "%2B");
|
||||
|
||||
form_urlencoded::parse(input.as_bytes())
|
||||
.map(|(key, val)| {
|
||||
[
|
||||
key.to_string(),
|
||||
if val.to_string().len() == 0 {
|
||||
str!()
|
||||
} else {
|
||||
str!('=')
|
||||
},
|
||||
val.to_string(),
|
||||
]
|
||||
.concat()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn file_url_to_fs_path(url: &str) -> String {
|
||||
if !is_file_url(url) {
|
||||
return str!();
|
||||
}
|
||||
|
||||
let cutoff_l = if cfg!(windows) { 8 } else { 7 };
|
||||
let mut fs_file_path: String = decode_url(url.to_string()[cutoff_l..].to_string());
|
||||
let url_fragment = get_url_fragment(url);
|
||||
if url_fragment != "" {
|
||||
let max_len = fs_file_path.len() - 1 - url_fragment.len();
|
||||
fs_file_path = fs_file_path[0..max_len].to_string();
|
||||
}
|
||||
|
||||
if cfg!(windows) {
|
||||
fs_file_path = fs_file_path.replace("/", "\\");
|
||||
}
|
||||
|
||||
// File paths should not be %-encoded
|
||||
decode_url(fs_file_path)
|
||||
}
|
||||
|
||||
pub fn get_url_fragment<T: AsRef<str>>(url: T) -> String {
|
||||
match Url::parse(url.as_ref()) {
|
||||
Ok(parsed_url) => parsed_url.fragment().unwrap_or("").to_string(),
|
||||
Err(_err) => str!(),
|
||||
pub fn is_url_and_has_protocol(input: &str) -> bool {
|
||||
match Url::parse(&input) {
|
||||
Ok(parsed_url) => {
|
||||
return parsed_url.scheme().len() > 0;
|
||||
}
|
||||
Err(_) => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_data_url<T: AsRef<str>>(url: T) -> bool {
|
||||
Url::parse(url.as_ref())
|
||||
.and_then(|u| Ok(u.scheme() == "data"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_file_url<T: AsRef<str>>(url: T) -> bool {
|
||||
Url::parse(url.as_ref())
|
||||
.and_then(|u| Ok(u.scheme() == "file"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_http_url<T: AsRef<str>>(url: T) -> bool {
|
||||
Url::parse(url.as_ref())
|
||||
.and_then(|u| Ok(u.scheme() == "http" || u.scheme() == "https"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn parse_data_url<T: AsRef<str>>(url: T) -> (String, Vec<u8>) {
|
||||
let parsed_url: Url = Url::parse(url.as_ref()).unwrap_or(Url::parse("data:,").unwrap());
|
||||
let path: String = parsed_url.path().to_string();
|
||||
pub fn parse_data_url(url: &Url) -> (String, String, Vec<u8>) {
|
||||
let path: String = url.path().to_string();
|
||||
let comma_loc: usize = path.find(',').unwrap_or(path.len());
|
||||
|
||||
let meta_data: String = path.chars().take(comma_loc).collect();
|
||||
let raw_data: String = path.chars().skip(comma_loc + 1).collect();
|
||||
// Split data URL into meta data and raw data
|
||||
let content_type: String = path.chars().take(comma_loc).collect();
|
||||
let data: String = path.chars().skip(comma_loc + 1).collect();
|
||||
|
||||
let text: String = decode_url(raw_data);
|
||||
// Parse meta data
|
||||
let (media_type, charset, is_base64) = parse_content_type(&content_type);
|
||||
|
||||
let meta_data_items: Vec<&str> = meta_data.split(';').collect();
|
||||
let mut media_type: String = str!();
|
||||
let mut encoding: &str = "";
|
||||
|
||||
let mut i: i8 = 0;
|
||||
for item in &meta_data_items {
|
||||
if i == 0 {
|
||||
media_type = str!(item);
|
||||
} else {
|
||||
if item.eq_ignore_ascii_case("base64")
|
||||
|| item.eq_ignore_ascii_case("utf8")
|
||||
|| item.eq_ignore_ascii_case("charset=UTF-8")
|
||||
{
|
||||
encoding = item;
|
||||
}
|
||||
}
|
||||
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
let data: Vec<u8> = if encoding.eq_ignore_ascii_case("base64") {
|
||||
// Parse raw data into vector of bytes
|
||||
let text: String = percent_decode_str(&data).decode_utf8_lossy().to_string();
|
||||
let blob: Vec<u8> = if is_base64 {
|
||||
base64::decode(&text).unwrap_or(vec![])
|
||||
} else {
|
||||
text.as_bytes().to_vec()
|
||||
};
|
||||
|
||||
(media_type, data)
|
||||
(media_type, charset, blob)
|
||||
}
|
||||
|
||||
pub fn resolve_url<T: AsRef<str>, U: AsRef<str>>(from: T, to: U) -> Result<String, ParseError> {
|
||||
let result = if is_http_url(to.as_ref()) {
|
||||
to.as_ref().to_string()
|
||||
} else {
|
||||
Url::parse(from.as_ref())?
|
||||
.join(to.as_ref())?
|
||||
.as_ref()
|
||||
.to_string()
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn url_has_protocol<T: AsRef<str>>(url: T) -> bool {
|
||||
Url::parse(url.as_ref())
|
||||
.and_then(|u| Ok(u.scheme().len() > 0))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn url_with_fragment(url: &str, fragment: &str) -> String {
|
||||
let mut result = str!(&url);
|
||||
|
||||
if !fragment.is_empty() {
|
||||
result += "#";
|
||||
result += fragment;
|
||||
pub fn resolve_url(from: &Url, to: &str) -> Url {
|
||||
match Url::parse(&to) {
|
||||
Ok(parsed_url) => parsed_url,
|
||||
Err(_) => match from.join(to) {
|
||||
Ok(joined) => joined,
|
||||
Err(_) => Url::parse("data:,").unwrap(),
|
||||
},
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
273
src/utils.rs
273
src/utils.rs
@@ -2,15 +2,14 @@ use reqwest::blocking::Client;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use url::Url;
|
||||
|
||||
use crate::opts::Options;
|
||||
use crate::url::{clean_url, file_url_to_fs_path, is_data_url, is_file_url, parse_data_url};
|
||||
use crate::url::{clean_url, parse_data_url};
|
||||
|
||||
const ANSI_COLOR_RED: &'static str = "\x1b[31m";
|
||||
const ANSI_COLOR_RESET: &'static str = "\x1b[0m";
|
||||
const INDENT: &'static str = " ";
|
||||
|
||||
const MAGIC: [[&[u8]; 2]; 18] = [
|
||||
// Image
|
||||
[b"GIF87a", b"image/gif"],
|
||||
@@ -36,26 +35,73 @@ const MAGIC: [[&[u8]; 2]; 18] = [
|
||||
];
|
||||
const PLAINTEXT_MEDIA_TYPES: &[&str] = &[
|
||||
"application/javascript",
|
||||
"application/json",
|
||||
"image/svg+xml",
|
||||
// "text/css",
|
||||
// "text/csv",
|
||||
// "text/html",
|
||||
// "text/javascript",
|
||||
// "text/plain",
|
||||
];
|
||||
|
||||
pub fn detect_media_type(data: &[u8], url: &str) -> String {
|
||||
for item in MAGIC.iter() {
|
||||
if data.starts_with(item[0]) {
|
||||
return String::from_utf8(item[1].to_vec()).unwrap();
|
||||
pub fn detect_media_type(data: &[u8], url: &Url) -> String {
|
||||
// At first attempt to read file's header
|
||||
for magic_item in MAGIC.iter() {
|
||||
if data.starts_with(magic_item[0]) {
|
||||
return String::from_utf8(magic_item[1].to_vec()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if url.to_lowercase().ends_with(".svg") {
|
||||
return str!("image/svg+xml");
|
||||
// If header didn't match any known magic signatures,
|
||||
// try to guess media type from file name
|
||||
let parts: Vec<&str> = url.path().split('/').collect();
|
||||
detect_media_type_by_file_name(parts.last().unwrap())
|
||||
}
|
||||
|
||||
pub fn detect_media_type_by_file_name(filename: &str) -> String {
|
||||
let filename_lowercased: &str = &filename.to_lowercase();
|
||||
let parts: Vec<&str> = filename_lowercased.split('.').collect();
|
||||
|
||||
let mime: &str = match parts.last() {
|
||||
Some(v) => match *v {
|
||||
"avi" => "video/avi",
|
||||
"bmp" => "image/bmp",
|
||||
"css" => "text/css",
|
||||
"flac" => "audio/flac",
|
||||
"gif" => "image/gif",
|
||||
"htm" | "html" => "text/html",
|
||||
"ico" => "image/x-icon",
|
||||
"jpeg" | "jpg" => "image/jpeg",
|
||||
"js" => "application/javascript",
|
||||
"json" => "application/json",
|
||||
"mp3" => "audio/mpeg",
|
||||
"mp4" | "m4v" => "video/mp4",
|
||||
"ogg" => "audio/ogg",
|
||||
"ogv" => "video/ogg",
|
||||
"pdf" => "application/pdf",
|
||||
"png" => "image/png",
|
||||
"svg" => "image/svg+xml",
|
||||
"swf" => "application/x-shockwave-flash",
|
||||
"tif" | "tiff" => "image/tiff",
|
||||
"txt" => "text/plain",
|
||||
"wav" => "audio/wav",
|
||||
"webp" => "image/webp",
|
||||
"woff" => "font/woff",
|
||||
"woff2" => "font/woff2",
|
||||
"xml" => "text/xml",
|
||||
&_ => "",
|
||||
},
|
||||
None => "",
|
||||
};
|
||||
|
||||
mime.to_string()
|
||||
}
|
||||
|
||||
pub fn indent(level: u32) -> String {
|
||||
let mut result: String = String::new();
|
||||
let mut l: u32 = level;
|
||||
|
||||
while l > 0 {
|
||||
result += " ";
|
||||
l -= 1;
|
||||
}
|
||||
|
||||
str!()
|
||||
result
|
||||
}
|
||||
|
||||
pub fn is_plaintext_media_type(media_type: &str) -> bool {
|
||||
@@ -63,54 +109,119 @@ pub fn is_plaintext_media_type(media_type: &str) -> bool {
|
||||
|| PLAINTEXT_MEDIA_TYPES.contains(&media_type.to_lowercase().as_str())
|
||||
}
|
||||
|
||||
pub fn indent(level: u32) -> String {
|
||||
let mut result = str!();
|
||||
let mut l: u32 = level;
|
||||
while l > 0 {
|
||||
result += INDENT;
|
||||
l -= 1;
|
||||
pub fn parse_content_type(content_type: &str) -> (String, String, bool) {
|
||||
let mut media_type: String = "text/plain".to_string();
|
||||
let mut charset: String = "US-ASCII".to_string();
|
||||
let mut is_base64: bool = false;
|
||||
|
||||
// Parse meta data
|
||||
let content_type_items: Vec<&str> = content_type.split(';').collect();
|
||||
let mut i: i8 = 0;
|
||||
for item in &content_type_items {
|
||||
if i == 0 {
|
||||
if item.trim().len() > 0 {
|
||||
media_type = item.trim().to_string();
|
||||
}
|
||||
} else {
|
||||
if item.trim().eq_ignore_ascii_case("base64") {
|
||||
is_base64 = true;
|
||||
} else if item.trim().starts_with("charset=") {
|
||||
charset = item.trim().chars().skip(8).collect();
|
||||
}
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
result
|
||||
|
||||
(media_type, charset, is_base64)
|
||||
}
|
||||
|
||||
pub fn retrieve_asset(
|
||||
cache: &mut HashMap<String, Vec<u8>>,
|
||||
client: &Client,
|
||||
parent_url: &str,
|
||||
url: &str,
|
||||
parent_url: &Url,
|
||||
url: &Url,
|
||||
options: &Options,
|
||||
depth: u32,
|
||||
) -> Result<(Vec<u8>, String, String), reqwest::Error> {
|
||||
if url.len() == 0 {
|
||||
// Provoke error
|
||||
client.get("").send()?;
|
||||
}
|
||||
|
||||
if is_data_url(&url) {
|
||||
let (media_type, data) = parse_data_url(url);
|
||||
Ok((data, url.to_string(), media_type))
|
||||
} else if is_file_url(&url) {
|
||||
// Check if parent_url is also file:///
|
||||
// (if not, then we don't embed the asset)
|
||||
if !is_file_url(&parent_url) {
|
||||
) -> Result<(Vec<u8>, Url, String, String), reqwest::Error> {
|
||||
if url.scheme() == "data" {
|
||||
let (media_type, charset, data) = parse_data_url(url);
|
||||
Ok((data, url.clone(), media_type, charset))
|
||||
} else if url.scheme() == "file" {
|
||||
// Check if parent_url is also file:/// (if not, then we don't embed the asset)
|
||||
if parent_url.scheme() != "file" {
|
||||
if !options.silent {
|
||||
eprintln!(
|
||||
"{}{}{} ({}){}",
|
||||
indent(depth).as_str(),
|
||||
if options.no_color { "" } else { ANSI_COLOR_RED },
|
||||
&url,
|
||||
"Security Error",
|
||||
if options.no_color {
|
||||
""
|
||||
} else {
|
||||
ANSI_COLOR_RESET
|
||||
},
|
||||
);
|
||||
}
|
||||
// Provoke error
|
||||
client.get("").send()?;
|
||||
}
|
||||
|
||||
let fs_file_path: String = file_url_to_fs_path(url);
|
||||
let path = Path::new(&fs_file_path);
|
||||
let path_buf: PathBuf = url.to_file_path().unwrap().clone();
|
||||
let path: &Path = path_buf.as_path();
|
||||
if path.exists() {
|
||||
if path.is_dir() {
|
||||
if !options.silent {
|
||||
eprintln!(
|
||||
"{}{}{} (is a directory){}",
|
||||
indent(depth).as_str(),
|
||||
if options.no_color { "" } else { ANSI_COLOR_RED },
|
||||
&url,
|
||||
if options.no_color {
|
||||
""
|
||||
} else {
|
||||
ANSI_COLOR_RESET
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Provoke error
|
||||
Err(client.get("").send().unwrap_err())
|
||||
} else {
|
||||
if !options.silent {
|
||||
eprintln!("{}{}", indent(depth).as_str(), &url);
|
||||
}
|
||||
|
||||
let file_blob: Vec<u8> = fs::read(&path).expect("Unable to read file");
|
||||
|
||||
Ok((
|
||||
file_blob.clone(),
|
||||
url.clone(),
|
||||
detect_media_type(&file_blob, url),
|
||||
"".to_string(),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
if !options.silent {
|
||||
eprintln!("{}{}", indent(depth).as_str(), &url);
|
||||
eprintln!(
|
||||
"{}{}{} (not found){}",
|
||||
indent(depth).as_str(),
|
||||
if options.no_color { "" } else { ANSI_COLOR_RED },
|
||||
&url,
|
||||
if options.no_color {
|
||||
""
|
||||
} else {
|
||||
ANSI_COLOR_RESET
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok((fs::read(&fs_file_path).expect(""), url.to_string(), str!()))
|
||||
} else {
|
||||
// Provoke error
|
||||
Err(client.get("").send().unwrap_err())
|
||||
}
|
||||
} else {
|
||||
let cache_key: String = clean_url(&url);
|
||||
let cache_key: String = clean_url(url.clone()).as_str().to_string();
|
||||
|
||||
if cache.contains_key(&cache_key) {
|
||||
// URL is in cache, we get and return it
|
||||
@@ -120,14 +231,15 @@ pub fn retrieve_asset(
|
||||
|
||||
Ok((
|
||||
cache.get(&cache_key).unwrap().to_vec(),
|
||||
url.to_string(),
|
||||
str!(),
|
||||
url.clone(),
|
||||
"".to_string(),
|
||||
"".to_string(),
|
||||
))
|
||||
} else {
|
||||
// URL not in cache, we retrieve the file
|
||||
match client.get(url).send() {
|
||||
Ok(mut response) => {
|
||||
if !options.ignore_errors && response.status() != 200 {
|
||||
match client.get(url.as_str()).send() {
|
||||
Ok(response) => {
|
||||
if !options.ignore_errors && response.status() != reqwest::StatusCode::OK {
|
||||
if !options.silent {
|
||||
eprintln!(
|
||||
"{}{}{} ({}){}",
|
||||
@@ -146,35 +258,74 @@ pub fn retrieve_asset(
|
||||
return Err(client.get("").send().unwrap_err());
|
||||
}
|
||||
|
||||
let res_url = response.url().to_string();
|
||||
let response_url: Url = response.url().clone();
|
||||
|
||||
if !options.silent {
|
||||
if url == res_url {
|
||||
if url.as_str() == response_url.as_str() {
|
||||
eprintln!("{}{}", indent(depth).as_str(), &url);
|
||||
} else {
|
||||
eprintln!("{}{} -> {}", indent(depth).as_str(), &url, &res_url);
|
||||
eprintln!("{}{} -> {}", indent(depth).as_str(), &url, &response_url);
|
||||
}
|
||||
}
|
||||
|
||||
let new_cache_key: String = clean_url(&res_url);
|
||||
let new_cache_key: String = clean_url(response_url.clone()).to_string();
|
||||
|
||||
// Convert response into a byte array
|
||||
let mut data: Vec<u8> = vec![];
|
||||
response.copy_to(&mut data)?;
|
||||
|
||||
// Attempt to obtain media type by reading the Content-Type header
|
||||
let media_type = response
|
||||
// Attempt to obtain media type and charset by reading Content-Type header
|
||||
let content_type: &str = response
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let (media_type, charset, _is_base64) = parse_content_type(&content_type);
|
||||
|
||||
// Convert response into a byte array
|
||||
let mut data: Vec<u8> = vec![];
|
||||
match response.bytes() {
|
||||
Ok(b) => {
|
||||
data = b.to_vec();
|
||||
}
|
||||
Err(error) => {
|
||||
if !options.silent {
|
||||
eprintln!(
|
||||
"{}{}{}{}",
|
||||
indent(depth).as_str(),
|
||||
if options.no_color { "" } else { ANSI_COLOR_RED },
|
||||
error,
|
||||
if options.no_color {
|
||||
""
|
||||
} else {
|
||||
ANSI_COLOR_RESET
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add retrieved resource to cache
|
||||
cache.insert(new_cache_key, data.clone());
|
||||
|
||||
Ok((data, res_url, media_type.to_string()))
|
||||
// Return
|
||||
Ok((data, response_url, media_type, charset))
|
||||
}
|
||||
Err(error) => {
|
||||
if !options.silent {
|
||||
eprintln!(
|
||||
"{}{}{} ({}){}",
|
||||
indent(depth).as_str(),
|
||||
if options.no_color { "" } else { ANSI_COLOR_RED },
|
||||
&url,
|
||||
error,
|
||||
if options.no_color {
|
||||
""
|
||||
} else {
|
||||
ANSI_COLOR_RESET
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Err(client.get("").send().unwrap_err())
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
tests/_data_/css/index.html
Normal file
11
tests/_data_/css/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<style>
|
||||
|
||||
@charset 'UTF-8';
|
||||
|
||||
@import 'style.css';
|
||||
|
||||
@import url(style.css);
|
||||
|
||||
@import url('style.css');
|
||||
|
||||
</style>
|
||||
1
tests/_data_/css/style.css
Normal file
1
tests/_data_/css/style.css
Normal file
@@ -0,0 +1 @@
|
||||
body{background-color:#000;color:#fff}
|
||||
@@ -3,8 +3,6 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' file:;" />
|
||||
<title>Local HTML file</title>
|
||||
<link href="style.css" rel="stylesheet" type="text/css" integrity="sha512-IWaCTORHkRhOWzcZeILSVmV6V6gPTHgNem6o6rsFAyaKTieDFkeeMrWjtO0DuWrX3bqZY46CVTZXUu0mia0qXQ==" crossorigin="anonymous" />
|
||||
<link href="style.css" rel="stylesheet" type="text/css" integrity="sha512-vWBzl4NE9oIg8NFOPAyOZbaam0UXWr6aDHPaY2kodSzAFl+mKoj/RMNc6C31NDqK4mE2i68IWxYWqWJPLCgPOw==" crossorigin="anonymous" />
|
||||
5
tests/_data_/noscript/image.svg
Normal file
5
tests/_data_/noscript/image.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1" baseProfile="full" width="300" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="red" />
|
||||
<circle cx="150" cy="100" r="80" fill="green" />
|
||||
<text x="150" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
1
tests/_data_/noscript/index.html
Normal file
1
tests/_data_/noscript/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<body><noscript><img src="image.svg" /></noscript></body>
|
||||
1
tests/_data_/noscript/nested.html
Normal file
1
tests/_data_/noscript/nested.html
Normal file
@@ -0,0 +1 @@
|
||||
<body><noscript><h1>JS is not active</h1><noscript><img src="image.svg" /></noscript></noscript></body>
|
||||
1
tests/_data_/noscript/script.html
Normal file
1
tests/_data_/noscript/script.html
Normal file
@@ -0,0 +1 @@
|
||||
<body><noscript><script>alert(1);</script><img src="image.svg" /></noscript></body>
|
||||
5
tests/_data_/svg/image.svg
Normal file
5
tests/_data_/svg/image.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1" baseProfile="full" width="300" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="red" />
|
||||
<circle cx="150" cy="100" r="80" fill="green" />
|
||||
<text x="150" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 296 B |
1
tests/_data_/svg/index.html
Normal file
1
tests/_data_/svg/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<div style="background-image: url('image.svg')"></div>
|
||||
9
tests/_data_/unusual_encodings/gb2312.html
Normal file
9
tests/_data_/unusual_encodings/gb2312.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=GB2312"/>
|
||||
<title>近七成人减少线下需求 银行数字化转型提速--经济·科技--人民网 </title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>近七成人减少线下需求 银行数字化转型提速</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,8 +12,8 @@ mod passing {
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn add_new_when_provided() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn add_new_when_provided() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-b")
|
||||
@@ -22,52 +22,48 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain newly added base URL
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<base href=\"http://localhost:8000/\"></base>\
|
||||
</head><body>Hello, World!</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keep_existing_when_none_provided() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn keep_existing_when_none_provided() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("data:text/html,<base href=\"http://localhost:8000/\" />Hello%2C%20World!")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain newly added base URL
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<base href=\"http://localhost:8000/\">\
|
||||
</head><body>Hello, World!</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn override_existing_when_provided() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn override_existing_when_provided() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-b")
|
||||
@@ -76,26 +72,24 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain newly added base URL
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<base href=\"http://localhost/\">\
|
||||
</head><body>Hello, World!</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_existing_when_empty_provided() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn set_existing_to_empty_when_empty_provided() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-b")
|
||||
@@ -104,20 +98,18 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain newly added base URL
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<base href=\"\">\
|
||||
</head><body>Hello, World!</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -9,32 +9,46 @@
|
||||
mod passing {
|
||||
use assert_cmd::prelude::*;
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use tempfile::NamedTempFile;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn print_version() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let out = cmd.arg("-V").output().unwrap();
|
||||
|
||||
// STDOUT should contain program name and version
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
format!("{} {}\n", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
|
||||
);
|
||||
fn print_help_information() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd.arg("-h").output().unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// STDOUT should contain program name, version, and usage information
|
||||
// TODO
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_target_input() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn print_version() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd.arg("-V").output().unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain program name and version
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
format!("{} {}\n", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_target_input() {
|
||||
let mut echo = Command::new("echo")
|
||||
.arg("Hello from STDIN")
|
||||
.stdout(Stdio::piped())
|
||||
@@ -43,70 +57,57 @@ mod passing {
|
||||
let echo_out = echo.stdout.take().unwrap();
|
||||
echo.wait().unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
cmd.stdin(echo_out);
|
||||
let out = cmd.arg("-M").arg("-").output().unwrap();
|
||||
|
||||
// STDOUT should contain HTML from STDIN
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML created out of STDIN
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head></head><body>Hello from STDIN\n</body></html>\n"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_import_string() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file_url_prefix: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let mut file_css = NamedTempFile::new()?;
|
||||
writeln!(file_css, "body{{background-color:#000;color:#fff}}")?;
|
||||
let mut file_html = NamedTempFile::new()?;
|
||||
writeln!(
|
||||
file_html,
|
||||
"\
|
||||
<style>\n\
|
||||
@charset 'UTF-8';\n\
|
||||
\n\
|
||||
@import '{file}{css_path}';\n\
|
||||
\n\
|
||||
@import url({file}{css_path});\n\
|
||||
\n\
|
||||
@import url('{file}{css_path}')\n\
|
||||
</style>\n\
|
||||
",
|
||||
file = file_url_prefix,
|
||||
css_path = str!(file_css.path().to_str().unwrap()).replace("\\", "/"),
|
||||
)?;
|
||||
let out = cmd.arg("-M").arg(file_html.path()).output().unwrap();
|
||||
fn css_import_string() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let path_html: &Path = Path::new("tests/_data_/css/index.html");
|
||||
let path_css: &Path = Path::new("tests/_data_/css/style.css");
|
||||
|
||||
// STDOUT should contain embedded CSS url()'s
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
"<html><head><style>\n@charset 'UTF-8';\n\n@import 'data:text/css;base64,Ym9keXtiYWNrZ3JvdW5kLWNvbG9yOiMwMDA7Y29sb3I6I2ZmZn0K';\n\n@import url('data:text/css;base64,Ym9keXtiYWNrZ3JvdW5kLWNvbG9yOiMwMDA7Y29sb3I6I2ZmZn0K');\n\n@import url('data:text/css;base64,Ym9keXtiYWNrZ3JvdW5kLWNvbG9yOiMwMDA7Y29sb3I6I2ZmZn0K')\n</style>\n\n</head><body></body></html>\n"
|
||||
);
|
||||
assert!(path_html.is_file());
|
||||
assert!(path_css.is_file());
|
||||
|
||||
// STDERR should list temporary files that got retrieved
|
||||
let out = cmd.arg("-M").arg(path_html.as_os_str()).output().unwrap();
|
||||
|
||||
// STDERR should list files that got retrieved
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file}{html_path}\n \
|
||||
{file}{css_path}\n \
|
||||
{file}{css_path}\n \
|
||||
{file}{css_path}\n\
|
||||
{file_url_html}\n \
|
||||
{file_url_css}\n \
|
||||
{file_url_css}\n \
|
||||
{file_url_css}\n\
|
||||
",
|
||||
file = file_url_prefix,
|
||||
html_path = str!(file_html.path().to_str().unwrap()).replace("\\", "/"),
|
||||
css_path = str!(file_css.path().to_str().unwrap()).replace("\\", "/"),
|
||||
file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(),
|
||||
file_url_css = Url::from_file_path(fs::canonicalize(&path_css).unwrap()).unwrap(),
|
||||
)
|
||||
);
|
||||
|
||||
// The exit code should be 0
|
||||
out.assert().code(0);
|
||||
// STDOUT should contain embedded CSS url()'s
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head><style>\n\n @charset \"UTF-8\";\n\n @import \"data:text/css;base64,Ym9keXtiYWNrZ3JvdW5kLWNvbG9yOiMwMDA7Y29sb3I6I2ZmZn0K\";\n\n @import url(\"data:text/css;base64,Ym9keXtiYWNrZ3JvdW5kLWNvbG9yOiMwMDA7Y29sb3I6I2ZmZn0K\");\n\n @import url(\"data:text/css;base64,Ym9keXtiYWNrZ3JvdW5kLWNvbG9yOiMwMDA7Y29sb3I6I2ZmZn0K\");\n\n</style>\n</head><body></body></html>\n"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,22 +125,20 @@ mod failing {
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn bad_input_empty_target() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn bad_input_empty_target() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd.arg("").output().unwrap();
|
||||
|
||||
// STDOUT should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stdout).unwrap(), "");
|
||||
|
||||
// STDERR should contain error description
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
"No target specified\n"
|
||||
);
|
||||
|
||||
// The exit code should be 1
|
||||
out.assert().code(1);
|
||||
// STDOUT should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stdout), "");
|
||||
|
||||
Ok(())
|
||||
// Exit code should be 1
|
||||
out.assert().code(1);
|
||||
}
|
||||
}
|
||||
@@ -11,29 +11,11 @@ mod passing {
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn bad_input_data_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
let out = cmd.arg("data:,Hello%2C%20World!").output().unwrap();
|
||||
|
||||
// STDOUT should contain HTML
|
||||
assert_eq!(std::str::from_utf8(&out.stdout).unwrap(), "");
|
||||
|
||||
// STDERR should contain error description
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stderr).unwrap(),
|
||||
"Unsupported data URL media type\n"
|
||||
);
|
||||
|
||||
// The exit code should be 1
|
||||
out.assert().code(1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use monolith::url::EMPTY_IMAGE_DATA_URL;
|
||||
|
||||
#[test]
|
||||
fn isolate_data_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn isolate_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-I")
|
||||
@@ -41,26 +23,24 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain isolated HTML
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-inline' data:;\"></meta>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-eval' 'unsafe-inline' data:;\"></meta>\
|
||||
</head><body>Hello, World!</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_css_from_data_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn remove_css_from_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-c")
|
||||
@@ -68,27 +48,25 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML with no CSS
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"style-src 'none';\"></meta>\
|
||||
<style></style>\
|
||||
</head><body>Hello</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_fonts_from_data_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn remove_fonts_from_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-F")
|
||||
@@ -96,27 +74,25 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML with no web fonts
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"font-src 'none';\"></meta>\
|
||||
<style></style>\
|
||||
</head><body>Hi</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_frames_from_data_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn remove_frames_from_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-f")
|
||||
@@ -124,26 +100,24 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML with no iframes
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"frame-src 'none'; child-src 'none';\"></meta>\
|
||||
</head><body><iframe src=\"\"></iframe>Hi</body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_images_from_data_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn remove_images_from_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-i")
|
||||
@@ -151,9 +125,12 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML with no images
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
format!(
|
||||
"<html>\
|
||||
<head>\
|
||||
@@ -164,22 +141,17 @@ mod passing {
|
||||
Hi\
|
||||
</body>\
|
||||
</html>\n",
|
||||
empty_image = empty_image!()
|
||||
empty_image = EMPTY_IMAGE_DATA_URL,
|
||||
)
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_js_from_data_url() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn remove_js_from_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-j")
|
||||
@@ -187,9 +159,12 @@ mod passing {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML with no JS
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html>\
|
||||
<head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'none';\"></meta>\
|
||||
@@ -198,37 +173,61 @@ mod passing {
|
||||
</html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗
|
||||
// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝
|
||||
// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use assert_cmd::prelude::*;
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn bad_input_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd.arg("data:,Hello%2C%20World!").output().unwrap();
|
||||
|
||||
// STDERR should contain error description
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
"Unsupported document media type\n"
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML
|
||||
assert_eq!(String::from_utf8_lossy(&out.stdout), "");
|
||||
|
||||
// Exit code should be 1
|
||||
out.assert().code(1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_disallow_local_assets_within_data_url_targets(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?;
|
||||
fn security_disallow_local_assets_within_data_url_targets() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("data:text/html,%3Cscript%20src=\"src/tests/data/basic/local-script.js\"%3E%3C/script%3E")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML with no JS in it
|
||||
assert_eq!(
|
||||
std::str::from_utf8(&out.stdout).unwrap(),
|
||||
"<html><head><script></script></head><body></body></html>\n"
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head><script src=\"data:application/javascript;base64,\"></script></head><body></body></html>\n"
|
||||
);
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(std::str::from_utf8(&out.stderr).unwrap(), "");
|
||||
|
||||
// The exit code should be 0
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
271
tests/cli/local_files.rs
Normal file
271
tests/cli/local_files.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use assert_cmd::prelude::*;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, MAIN_SEPARATOR};
|
||||
use std::process::Command;
|
||||
use url::Url;
|
||||
|
||||
use monolith::url::EMPTY_IMAGE_DATA_URL;
|
||||
|
||||
#[test]
|
||||
fn local_file_target_input_relative_target_path() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let cwd_normalized: String = env::current_dir()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("\\", "/");
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg(format!(
|
||||
"tests{s}_data_{s}basic{s}local-file.html",
|
||||
s = MAIN_SEPARATOR
|
||||
))
|
||||
.output()
|
||||
.unwrap();
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// STDERR should contain list of retrieved file URLs, two missing
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file}{cwd}/tests/_data_/basic/local-file.html\n \
|
||||
{file}{cwd}/tests/_data_/basic/local-style.css\n \
|
||||
{file}{cwd}/tests/_data_/basic/local-style-does-not-exist.css (not found)\n \
|
||||
{file}{cwd}/tests/_data_/basic/monolith.png (not found)\n \
|
||||
{file}{cwd}/tests/_data_/basic/local-script.js\n\
|
||||
",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML from the local file
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"\
|
||||
<!DOCTYPE html><html lang=\"en\"><head>\n \
|
||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n \
|
||||
<title>Local HTML file</title>\n \
|
||||
<link href=\"data:text/css;base64,Ym9keSB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogICAgY29sb3I6ICNmZmY7Cn0K\" rel=\"stylesheet\" type=\"text/css\">\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n \
|
||||
<img alt=\"\">\n \
|
||||
<a href=\"file://local-file.html/\">Tricky href</a>\n \
|
||||
<a href=\"https://github.com/Y2Z/monolith\">Remote URL</a>\n \
|
||||
<script src=\"data:application/javascript;base64,ZG9jdW1lbnQuYm9keS5zdHlsZS5iYWNrZ3JvdW5kQ29sb3IgPSAiZ3JlZW4iOwpkb2N1bWVudC5ib2R5LnN0eWxlLmNvbG9yID0gInJlZCI7Cg==\"></script>\n\n\n\n\
|
||||
</body></html>\n\
|
||||
"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_file_target_input_absolute_target_path() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let path_html: &Path = Path::new("tests/_data_/basic/local-file.html");
|
||||
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-Ijci")
|
||||
.arg(path_html.as_os_str())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should contain only the target file
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"{file_url_html}\n",
|
||||
file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(),
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML from the local file
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
format!(
|
||||
"\
|
||||
<!DOCTYPE html><html lang=\"en\"><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-eval' 'unsafe-inline' data:; style-src 'none'; script-src 'none'; img-src data:;\"></meta>\n \
|
||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n \
|
||||
<title>Local HTML file</title>\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n \
|
||||
<img src=\"{empty_image}\" alt=\"\">\n \
|
||||
<a href=\"file://local-file.html/\">Tricky href</a>\n \
|
||||
<a href=\"https://github.com/Y2Z/monolith\">Remote URL</a>\n \
|
||||
<script></script>\n\n\n\n\
|
||||
</body></html>\n\
|
||||
",
|
||||
empty_image = EMPTY_IMAGE_DATA_URL
|
||||
)
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_file_url_target_input() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let cwd_normalized: String = env::current_dir()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("\\", "/");
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-cji")
|
||||
.arg(format!(
|
||||
"{file}{cwd}/tests/_data_/basic/local-file.html",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
))
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should contain list of retrieved file URLs
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"{file}{cwd}/tests/_data_/basic/local-file.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML from the local file
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
format!(
|
||||
"\
|
||||
<!DOCTYPE html><html lang=\"en\"><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"style-src 'none'; script-src 'none'; img-src data:;\"></meta>\n \
|
||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n \
|
||||
<title>Local HTML file</title>\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n \
|
||||
<link rel=\"stylesheet\" type=\"text/css\">\n</head>\n\n<body>\n \
|
||||
<img src=\"{empty_image}\" alt=\"\">\n \
|
||||
<a href=\"file://local-file.html/\">Tricky href</a>\n \
|
||||
<a href=\"https://github.com/Y2Z/monolith\">Remote URL</a>\n \
|
||||
<script></script>\n\n\n\n\
|
||||
</body></html>\n\
|
||||
",
|
||||
empty_image = EMPTY_IMAGE_DATA_URL
|
||||
)
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embed_file_url_local_asset_within_style_attribute() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let path_html: &Path = Path::new("tests/_data_/svg/index.html");
|
||||
let path_svg: &Path = Path::new("tests/_data_/svg/image.svg");
|
||||
|
||||
let out = cmd.arg("-M").arg(path_html.as_os_str()).output().unwrap();
|
||||
|
||||
// STDERR should list files that got retrieved
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file_url_html}\n \
|
||||
{file_url_svg}\n\
|
||||
",
|
||||
file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(),
|
||||
file_url_svg = Url::from_file_path(fs::canonicalize(&path_svg).unwrap()).unwrap(),
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML with date URL for background-image in it
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head></head><body><div style=\"background-image: url("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGJhc2VQcm9maWxlPSJmdWxsIiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InJlZCIgLz4KICAgIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwMCIgcj0iODAiIGZpbGw9ImdyZWVuIiAvPgogICAgPHRleHQgeD0iMTUwIiB5PSIxMjUiIGZvbnQtc2l6ZT0iNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIj5TVkc8L3RleHQ+Cjwvc3ZnPgo=")\"></div>\n</body></html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discard_integrity_for_local_files() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let cwd_normalized: String = env::current_dir()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace("\\", "/");
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-i")
|
||||
.arg(if cfg!(windows) {
|
||||
format!(
|
||||
"{file}{cwd}/tests/_data_/integrity/index.html",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{file}{cwd}/tests/_data_/integrity/index.html",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
})
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should contain list of retrieved file URLs
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file}{cwd}/tests/_data_/integrity/index.html\n \
|
||||
{file}{cwd}/tests/_data_/integrity/style.css\n \
|
||||
{file}{cwd}/tests/_data_/integrity/style.css\n \
|
||||
{file}{cwd}/tests/_data_/integrity/script.js\n \
|
||||
{file}{cwd}/tests/_data_/integrity/script.js\n\
|
||||
",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML from the local file; integrity attributes should be missing
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
format!(
|
||||
"\
|
||||
<!DOCTYPE html><html lang=\"en\"><head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"img-src data:;\"></meta>\n \
|
||||
<title>Local HTML file</title>\n \
|
||||
<link href=\"data:text/css;base64,Ym9keSB7CiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjMDAwOwogICAgY29sb3I6ICNGRkY7Cn0K\" rel=\"stylesheet\" type=\"text/css\" crossorigin=\"anonymous\">\n \
|
||||
<link href=\"style.css\" rel=\"stylesheet\" type=\"text/css\" crossorigin=\"anonymous\">\n</head>\n\n<body>\n \
|
||||
<p>This page should have black background and white foreground, but only when served via http: (not via file:)</p>\n \
|
||||
<script src=\"data:application/javascript;base64,ZnVuY3Rpb24gbm9vcCgpIHsKICAgIGNvbnNvbGUubG9nKCJtb25vbGl0aCIpOwp9Cg==\"></script>\n \
|
||||
<script src=\"script.js\"></script>\n\n\n\n\
|
||||
</body></html>\n\
|
||||
"
|
||||
)
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ mod base_url;
|
||||
mod basic;
|
||||
mod data_url;
|
||||
mod local_files;
|
||||
mod noscript;
|
||||
mod unusual_encodings;
|
||||
170
tests/cli/noscript.rs
Normal file
170
tests/cli/noscript.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use assert_cmd::prelude::*;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn parse_noscript_contents() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let path_html: &Path = Path::new("tests/_data_/noscript/index.html");
|
||||
let path_svg: &Path = Path::new("tests/_data_/noscript/image.svg");
|
||||
|
||||
let out = cmd.arg("-M").arg(path_html.as_os_str()).output().unwrap();
|
||||
|
||||
// STDERR should contain target HTML and embedded SVG files
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file_url_html}\n \
|
||||
{file_url_svg}\n\
|
||||
",
|
||||
file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(),
|
||||
file_url_svg = Url::from_file_path(fs::canonicalize(&path_svg).unwrap()).unwrap(),
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML with no CSS
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head></head><body><noscript><img src=\"data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGJhc2VQcm9maWxlPSJmdWxsIiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InJlZCIgLz4KICAgIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwMCIgcj0iODAiIGZpbGw9ImdyZWVuIiAvPgogICAgPHRleHQgeD0iMTUwIiB5PSIxMjUiIGZvbnQtc2l6ZT0iNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIj5TVkc8L3RleHQ+Cjwvc3ZnPgo=\"></noscript>\n</body></html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwrap_noscript_contents() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let path_html: &Path = Path::new("tests/_data_/noscript/index.html");
|
||||
let path_svg: &Path = Path::new("tests/_data_/noscript/image.svg");
|
||||
|
||||
let out = cmd.arg("-Mn").arg(path_html.as_os_str()).output().unwrap();
|
||||
|
||||
// STDERR should contain target HTML and embedded SVG files
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file_url_html}\n \
|
||||
{file_url_svg}\n\
|
||||
",
|
||||
file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(),
|
||||
file_url_svg = Url::from_file_path(fs::canonicalize(&path_svg).unwrap()).unwrap(),
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML with no CSS
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head></head><body><!--noscript--><img src=\"data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGJhc2VQcm9maWxlPSJmdWxsIiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InJlZCIgLz4KICAgIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwMCIgcj0iODAiIGZpbGw9ImdyZWVuIiAvPgogICAgPHRleHQgeD0iMTUwIiB5PSIxMjUiIGZvbnQtc2l6ZT0iNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIj5TVkc8L3RleHQ+Cjwvc3ZnPgo=\"><!--/noscript-->\n</body></html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwrap_noscript_contents_nested() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let path_html: &Path = Path::new("tests/_data_/noscript/nested.html");
|
||||
let path_svg: &Path = Path::new("tests/_data_/noscript/image.svg");
|
||||
|
||||
let out = cmd.arg("-Mn").arg(path_html.as_os_str()).output().unwrap();
|
||||
|
||||
// STDERR should contain target HTML and embedded SVG files
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file_url_html}\n \
|
||||
{file_url_svg}\n\
|
||||
",
|
||||
file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(),
|
||||
file_url_svg = Url::from_file_path(fs::canonicalize(&path_svg).unwrap()).unwrap(),
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML with no CSS
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head></head><body><!--noscript--><h1>JS is not active</h1><!--noscript--><img src=\"data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGJhc2VQcm9maWxlPSJmdWxsIiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InJlZCIgLz4KICAgIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwMCIgcj0iODAiIGZpbGw9ImdyZWVuIiAvPgogICAgPHRleHQgeD0iMTUwIiB5PSIxMjUiIGZvbnQtc2l6ZT0iNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIj5TVkc8L3RleHQ+Cjwvc3ZnPgo=\"><!--/noscript--><!--/noscript-->\n</body></html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwrap_noscript_contents_with_script() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let path_html: &Path = Path::new("tests/_data_/noscript/script.html");
|
||||
let path_svg: &Path = Path::new("tests/_data_/noscript/image.svg");
|
||||
|
||||
let out = cmd.arg("-Mn").arg(path_html.as_os_str()).output().unwrap();
|
||||
|
||||
// STDERR should contain target HTML and embedded SVG files
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"\
|
||||
{file_url_html}\n \
|
||||
{file_url_svg}\n\
|
||||
",
|
||||
file_url_html = Url::from_file_path(fs::canonicalize(&path_html).unwrap()).unwrap(),
|
||||
file_url_svg = Url::from_file_path(fs::canonicalize(&path_svg).unwrap()).unwrap(),
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain HTML with no CSS
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html>\
|
||||
<head></head>\
|
||||
<body>\
|
||||
<!--noscript-->\
|
||||
<img src=\"data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGJhc2VQcm9maWxlPSJmdWxsIiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InJlZCIgLz4KICAgIDxjaXJjbGUgY3g9IjE1MCIgY3k9IjEwMCIgcj0iODAiIGZpbGw9ImdyZWVuIiAvPgogICAgPHRleHQgeD0iMTUwIiB5PSIxMjUiIGZvbnQtc2l6ZT0iNjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIj5TVkc8L3RleHQ+Cjwvc3ZnPgo=\">\
|
||||
<!--/noscript-->\n\
|
||||
</body>\
|
||||
</html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwrap_noscript_contents_attr_data_url() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-n")
|
||||
.arg("data:text/html,<noscript class=\"\">test</noscript>")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain unwrapped contents of NOSCRIPT element
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html><head><!--noscript class=\"\"-->test<!--/noscript--></head><body></body></html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
}
|
||||
239
tests/cli/unusual_encodings.rs
Normal file
239
tests/cli/unusual_encodings.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use assert_cmd::prelude::*;
|
||||
use encoding_rs::Encoding;
|
||||
use std::env;
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[test]
|
||||
fn properly_save_document_with_gb2312() {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let cwd_normalized: String = cwd.to_str().unwrap().replace("\\", "/");
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg(format!(
|
||||
"tests{s}_data_{s}unusual_encodings{s}gb2312.html",
|
||||
s = MAIN_SEPARATOR
|
||||
))
|
||||
.output()
|
||||
.unwrap();
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// STDERR should contain only the target file
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"{file}{cwd}/tests/_data_/unusual_encodings/gb2312.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain original document without any modificatons
|
||||
let s: String;
|
||||
if let Some(encoding) = Encoding::for_label(b"gb2312") {
|
||||
let (string, _, _) = encoding.decode(&out.stdout);
|
||||
s = string.to_string();
|
||||
} else {
|
||||
s = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
}
|
||||
assert_eq!(
|
||||
s,
|
||||
"<html>\
|
||||
<head>\n \
|
||||
<meta http-equiv=\"content-type\" content=\"text/html;charset=GB2312\">\n \
|
||||
<title>近七成人减少线下需求\u{3000}银行数字化转型提速--经济·科技--人民网 </title>\n\
|
||||
</head>\n\
|
||||
<body>\n \
|
||||
<h1>近七成人减少线下需求\u{3000}银行数字化转型提速</h1>\n\n\n\
|
||||
</body>\
|
||||
</html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properly_save_document_with_gb2312_from_stdin() {
|
||||
let mut echo = Command::new("cat")
|
||||
.arg(format!(
|
||||
"tests{s}_data_{s}unusual_encodings{s}gb2312.html",
|
||||
s = MAIN_SEPARATOR
|
||||
))
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let echo_out = echo.stdout.take().unwrap();
|
||||
echo.wait().unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
cmd.stdin(echo_out);
|
||||
let out = cmd.arg("-M").arg("-").output().unwrap();
|
||||
|
||||
// STDERR should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stderr), "");
|
||||
|
||||
// STDOUT should contain HTML created out of STDIN
|
||||
let s: String;
|
||||
if let Some(encoding) = Encoding::for_label(b"gb2312") {
|
||||
let (string, _, _) = encoding.decode(&out.stdout);
|
||||
s = string.to_string();
|
||||
} else {
|
||||
s = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
}
|
||||
assert_eq!(
|
||||
s,
|
||||
"<html>\
|
||||
<head>\n \
|
||||
<meta http-equiv=\"content-type\" content=\"text/html;charset=GB2312\">\n \
|
||||
<title>近七成人减少线下需求\u{3000}银行数字化转型提速--经济·科技--人民网 </title>\n\
|
||||
</head>\n\
|
||||
<body>\n \
|
||||
<h1>近七成人减少线下需求\u{3000}银行数字化转型提速</h1>\n\n\n\
|
||||
</body>\
|
||||
</html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properly_save_document_with_gb2312_custom_charset() {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let cwd_normalized: String = cwd.to_str().unwrap().replace("\\", "/");
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-C")
|
||||
.arg("utf8")
|
||||
.arg(format!(
|
||||
"tests{s}_data_{s}unusual_encodings{s}gb2312.html",
|
||||
s = MAIN_SEPARATOR
|
||||
))
|
||||
.output()
|
||||
.unwrap();
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// STDERR should contain only the target file
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"{file}{cwd}/tests/_data_/unusual_encodings/gb2312.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain original document without any modificatons
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout).to_string(),
|
||||
"<html>\
|
||||
<head>\n \
|
||||
<meta http-equiv=\"content-type\" content=\"text/html;charset=utf8\">\n \
|
||||
<title>近七成人减少线下需求\u{3000}银行数字化转型提速--经济·科技--人民网 </title>\n\
|
||||
</head>\n\
|
||||
<body>\n \
|
||||
<h1>近七成人减少线下需求\u{3000}银行数字化转型提速</h1>\n\n\n\
|
||||
</body>\
|
||||
</html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properly_save_document_with_gb2312_custom_charset_bad() {
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg("-C")
|
||||
.arg("utf0")
|
||||
.arg(format!(
|
||||
"tests{s}_data_{s}unusual_encodings{s}gb2312.html",
|
||||
s = MAIN_SEPARATOR
|
||||
))
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// STDERR should contain error message
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
"Unknown encoding: utf0\n"
|
||||
);
|
||||
|
||||
// STDOUT should be empty
|
||||
assert_eq!(String::from_utf8_lossy(&out.stdout).to_string(), "");
|
||||
|
||||
// Exit code should be 1
|
||||
out.assert().code(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗
|
||||
// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝
|
||||
// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use assert_cmd::prelude::*;
|
||||
use std::env;
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn change_iso88591_to_utf8_to_properly_display_html_entities() {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let cwd_normalized: String = cwd.to_str().unwrap().replace("\\", "/");
|
||||
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
|
||||
let out = cmd
|
||||
.arg("-M")
|
||||
.arg(format!(
|
||||
"tests{s}_data_{s}unusual_encodings{s}iso-8859-1.html",
|
||||
s = MAIN_SEPARATOR
|
||||
))
|
||||
.output()
|
||||
.unwrap();
|
||||
let file_url_protocol: &str = if cfg!(windows) { "file:///" } else { "file://" };
|
||||
|
||||
// STDERR should contain only the target file
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
format!(
|
||||
"{file}{cwd}/tests/_data_/unusual_encodings/iso-8859-1.html\n",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd_normalized,
|
||||
)
|
||||
);
|
||||
|
||||
// STDOUT should contain original document but with UTF-8 charset
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
"<html>\
|
||||
<head>\n \
|
||||
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">\n \
|
||||
</head>\n \
|
||||
<body>\n \
|
||||
<20> Some Company\n \
|
||||
\n\n</body>\
|
||||
</html>\n"
|
||||
);
|
||||
|
||||
// Exit code should be 0
|
||||
out.assert().code(0);
|
||||
}
|
||||
}
|
||||
@@ -8,35 +8,35 @@
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::css;
|
||||
use crate::opts::Options;
|
||||
use monolith::css;
|
||||
use monolith::opts::Options;
|
||||
use monolith::url::EMPTY_IMAGE_DATA_URL;
|
||||
|
||||
#[test]
|
||||
fn empty_input() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("data:,").unwrap();
|
||||
let options = Options::default();
|
||||
|
||||
assert_eq!(css::embed_css(cache, &client, "", "", &options, 0), "");
|
||||
assert_eq!(
|
||||
css::embed_css(cache, &client, &document_url, "", &options, 0),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_if_empty() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let options = Options::default();
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(
|
||||
cache,
|
||||
&client,
|
||||
"https://doesntmatter.local/",
|
||||
"\t \t ",
|
||||
&options,
|
||||
0,
|
||||
),
|
||||
css::embed_css(cache, &client, &document_url, "\t \t ", &options, 0,),
|
||||
""
|
||||
);
|
||||
}
|
||||
@@ -45,6 +45,7 @@ mod passing {
|
||||
fn style_exclude_unquoted_images() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
@@ -58,23 +59,16 @@ mod passing {
|
||||
height: calc(100vh - 10pt)";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(
|
||||
cache,
|
||||
&client,
|
||||
"https://doesntmatter.local/",
|
||||
&STYLE,
|
||||
&options,
|
||||
0,
|
||||
),
|
||||
css::embed_css(cache, &client, &document_url, &STYLE, &options, 0,),
|
||||
format!(
|
||||
"/* border: none;*/\
|
||||
background-image: url('{empty_image}'); \
|
||||
list-style: url('{empty_image}');\
|
||||
background-image: url(\"{empty_image}\"); \
|
||||
list-style: url(\"{empty_image}\");\
|
||||
width:99.998%; \
|
||||
margin-top: -20px; \
|
||||
line-height: -1; \
|
||||
height: calc(100vh - 10pt)",
|
||||
empty_image = empty_image!()
|
||||
empty_image = EMPTY_IMAGE_DATA_URL
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -83,6 +77,7 @@ mod passing {
|
||||
fn style_exclude_single_quoted_images() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("data:,").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
@@ -96,16 +91,16 @@ mod passing {
|
||||
height: calc(100vh - 10pt)";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(cache, &client, "", &STYLE, &options, 0),
|
||||
css::embed_css(cache, &client, &document_url, &STYLE, &options, 0),
|
||||
format!(
|
||||
"/* border: none;*/\
|
||||
background-image: url('{empty_image}'); \
|
||||
list-style: url('{empty_image}');\
|
||||
background-image: url(\"{empty_image}\"); \
|
||||
list-style: url(\"{empty_image}\");\
|
||||
width:99.998%; \
|
||||
margin-top: -20px; \
|
||||
line-height: -1; \
|
||||
height: calc(100vh - 10pt)",
|
||||
empty_image = empty_image!()
|
||||
empty_image = EMPTY_IMAGE_DATA_URL
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -114,19 +109,20 @@ mod passing {
|
||||
fn style_block() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("file:///").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
const CSS: &str = "\
|
||||
#id.class-name:not(:nth-child(3n+0)) {\n \
|
||||
// border: none;\n \
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=');\n\
|
||||
background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=\");\n\
|
||||
}\n\
|
||||
\n\
|
||||
html > body {}";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(cache, &client, "file:///", &CSS, &options, 0),
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0),
|
||||
CSS
|
||||
);
|
||||
}
|
||||
@@ -135,6 +131,7 @@ mod passing {
|
||||
fn attribute_selectors() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
@@ -143,38 +140,42 @@ mod passing {
|
||||
/* Attribute exists */
|
||||
}
|
||||
|
||||
[data-value='foo'] {
|
||||
[data-value=\"foo\"] {
|
||||
/* Attribute has this exact value */
|
||||
}
|
||||
|
||||
[data-value*='foo'] {
|
||||
[data-value*=\"foo\"] {
|
||||
/* Attribute value contains this value somewhere in it */
|
||||
}
|
||||
|
||||
[data-value~='foo'] {
|
||||
[data-value~=\"foo\"] {
|
||||
/* Attribute has this value in a space-separated list somewhere */
|
||||
}
|
||||
|
||||
[data-value^='foo'] {
|
||||
[data-value^=\"foo\"] {
|
||||
/* Attribute value starts with this */
|
||||
}
|
||||
|
||||
[data-value|='foo'] {
|
||||
[data-value|=\"foo\"] {
|
||||
/* Attribute value starts with this in a dash-separated list */
|
||||
}
|
||||
|
||||
[data-value$='foo'] {
|
||||
[data-value$=\"foo\"] {
|
||||
/* Attribute value ends with this */
|
||||
}
|
||||
";
|
||||
|
||||
assert_eq!(css::embed_css(cache, &client, "", &CSS, &options, 0), CSS);
|
||||
assert_eq!(
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0),
|
||||
CSS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_string() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
@@ -187,20 +188,13 @@ mod passing {
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(
|
||||
cache,
|
||||
&client,
|
||||
"https://doesntmatter.local/",
|
||||
&CSS,
|
||||
&options,
|
||||
0,
|
||||
),
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0,),
|
||||
"\
|
||||
@charset 'UTF-8';\n\
|
||||
@charset \"UTF-8\";\n\
|
||||
\n\
|
||||
@import 'data:text/css;base64,aHRtbHtiYWNrZ3JvdW5kLWNvbG9yOiMwMDB9';\n\
|
||||
@import \"data:text/css;base64,aHRtbHtiYWNrZ3JvdW5kLWNvbG9yOiMwMDB9\";\n\
|
||||
\n\
|
||||
@import url('data:text/css;base64,aHRtbHtjb2xvcjojZmZmfQ==')\n\
|
||||
@import url(\"data:text/css;base64,aHRtbHtjb2xvcjojZmZmfQ==\")\n\
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -209,6 +203,7 @@ mod passing {
|
||||
fn hash_urls() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
@@ -223,14 +218,7 @@ mod passing {
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(
|
||||
cache,
|
||||
&client,
|
||||
"https://doesntmatter.local/",
|
||||
&CSS,
|
||||
&options,
|
||||
0,
|
||||
),
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0,),
|
||||
CSS
|
||||
);
|
||||
}
|
||||
@@ -239,6 +227,7 @@ mod passing {
|
||||
fn transform_percentages_and_degrees() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
@@ -251,14 +240,7 @@ mod passing {
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(
|
||||
cache,
|
||||
&client,
|
||||
"https://doesntmatter.local/",
|
||||
&CSS,
|
||||
&options,
|
||||
0,
|
||||
),
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0,),
|
||||
CSS
|
||||
);
|
||||
}
|
||||
@@ -267,6 +249,7 @@ mod passing {
|
||||
fn unusual_indents() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
@@ -281,14 +264,7 @@ mod passing {
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(
|
||||
cache,
|
||||
&client,
|
||||
"https://doesntmatter.local/",
|
||||
&CSS,
|
||||
&options,
|
||||
0,
|
||||
),
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0,),
|
||||
CSS
|
||||
);
|
||||
}
|
||||
@@ -297,6 +273,7 @@ mod passing {
|
||||
fn exclude_fonts() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("https://doesntmatter.local/").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.no_fonts = true;
|
||||
options.silent = true;
|
||||
@@ -320,30 +297,74 @@ mod passing {
|
||||
font-family: 'My Font' Verdana\n\
|
||||
}\n\
|
||||
";
|
||||
|
||||
const CSS_OUT: &str = " \
|
||||
\n\
|
||||
\n\
|
||||
#identifier {\n \
|
||||
font-family: 'My Font' Arial\n\
|
||||
font-family: \"My Font\" Arial\n\
|
||||
}\n\
|
||||
\n \
|
||||
\n\
|
||||
\n\
|
||||
div {\n \
|
||||
font-family: 'My Font' Verdana\n\
|
||||
font-family: \"My Font\" Verdana\n\
|
||||
}\n\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(
|
||||
cache,
|
||||
&client,
|
||||
"https://doesntmatter.local/",
|
||||
&CSS,
|
||||
&options,
|
||||
0,
|
||||
),
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0,),
|
||||
CSS_OUT
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("data:,").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
const CSS: &str = "\
|
||||
#language a[href=\"#translations\"]:before {\n\
|
||||
content: url(data:,) \"\\A\";\n\
|
||||
white-space: pre }\n\
|
||||
";
|
||||
const CSS_OUT: &str = "\
|
||||
#language a[href=\"#translations\"]:before {\n\
|
||||
content: url(\"data:text/plain;base64,\") \"\\a \";\n\
|
||||
white-space: pre }\n\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0,),
|
||||
CSS_OUT
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ie_css_hack() {
|
||||
let cache = &mut HashMap::new();
|
||||
let client = Client::new();
|
||||
let document_url: Url = Url::parse("data:,").unwrap();
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
const CSS: &str = "\
|
||||
div#p>svg>foreignObject>section:not(\\9) {\n\
|
||||
width: 300px;\n\
|
||||
width: 500px\\9;\n\
|
||||
}\n\
|
||||
";
|
||||
const CSS_OUT: &str = "\
|
||||
div#p>svg>foreignObject>section:not(\\9) {\n\
|
||||
width: 300px;\n\
|
||||
width: 500px\t;\n\
|
||||
}\n\
|
||||
";
|
||||
|
||||
assert_eq!(
|
||||
css::embed_css(cache, &client, &document_url, &CSS, &options, 0,),
|
||||
CSS_OUT
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::css;
|
||||
use monolith::css;
|
||||
|
||||
#[test]
|
||||
fn backrgound() {
|
||||
@@ -64,7 +64,7 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::css;
|
||||
use monolith::css;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
@@ -1,3 +1,2 @@
|
||||
mod embed_css;
|
||||
mod enquote;
|
||||
mod is_image_url_prop;
|
||||
@@ -9,12 +9,12 @@
|
||||
mod passing {
|
||||
use html5ever::serialize::{serialize, SerializeOpts};
|
||||
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let html = "<div>text</div>";
|
||||
let mut dom = html::html_to_dom(&html);
|
||||
let mut dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
dom = html::add_favicon(&dom.document, "I_AM_A_FAVICON_DATA_URL".to_string());
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn empty_input_sha256() {
|
||||
@@ -51,7 +51,7 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn empty_hash() {
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::html;
|
||||
use crate::opts::Options;
|
||||
use monolith::html;
|
||||
use monolith::opts::Options;
|
||||
|
||||
#[test]
|
||||
fn isolated() {
|
||||
@@ -16,7 +16,10 @@ mod passing {
|
||||
options.isolate = true;
|
||||
let csp_content = html::compose_csp(&options);
|
||||
|
||||
assert_eq!(csp_content, "default-src 'unsafe-inline' data:;");
|
||||
assert_eq!(
|
||||
csp_content,
|
||||
"default-src 'unsafe-eval' 'unsafe-inline' data:;"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -75,6 +78,6 @@ mod passing {
|
||||
options.no_images = true;
|
||||
let csp_content = html::compose_csp(&options);
|
||||
|
||||
assert_eq!(csp_content, "default-src 'unsafe-inline' data:; style-src 'none'; font-src 'none'; frame-src 'none'; child-src 'none'; script-src 'none'; img-src data:;");
|
||||
assert_eq!(csp_content, "default-src 'unsafe-eval' 'unsafe-inline' data:; style-src 'none'; font-src 'none'; frame-src 'none'; child-src 'none'; script-src 'none'; img-src data:;");
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,15 @@
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use chrono::prelude::*;
|
||||
use reqwest::Url;
|
||||
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn http_url() {
|
||||
let url = "http://192.168.1.1/";
|
||||
let url: Url = Url::parse("http://192.168.1.1/").unwrap();
|
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let metadata_comment: String = html::create_metadata_tag(url);
|
||||
let metadata_comment: String = html::create_metadata_tag(&url);
|
||||
|
||||
assert_eq!(
|
||||
metadata_comment,
|
||||
@@ -31,9 +32,9 @@ mod passing {
|
||||
|
||||
#[test]
|
||||
fn file_url() {
|
||||
let url = "file:///home/monolith/index.html";
|
||||
let url: Url = Url::parse("file:///home/monolith/index.html").unwrap();
|
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let metadata_comment: String = html::create_metadata_tag(url);
|
||||
let metadata_comment: String = html::create_metadata_tag(&url);
|
||||
|
||||
assert_eq!(
|
||||
metadata_comment,
|
||||
@@ -48,9 +49,9 @@ mod passing {
|
||||
|
||||
#[test]
|
||||
fn data_url() {
|
||||
let url = "data:text/html,Hello%2C%20World!";
|
||||
let url: Url = Url::parse("data:text/html,Hello%2C%20World!").unwrap();
|
||||
let timestamp = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
|
||||
let metadata_comment: String = html::create_metadata_tag(url);
|
||||
let metadata_comment: String = html::create_metadata_tag(&url);
|
||||
|
||||
assert_eq!(
|
||||
metadata_comment,
|
||||
@@ -63,20 +64,3 @@ mod passing {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ███████╗ █████╗ ██╗██╗ ██╗███╗ ██╗ ██████╗
|
||||
// ██╔════╝██╔══██╗██║██║ ██║████╗ ██║██╔════╝
|
||||
// █████╗ ███████║██║██║ ██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔══╝ ██╔══██║██║██║ ██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║██║███████╗██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::html;
|
||||
|
||||
#[test]
|
||||
fn empty_string() {
|
||||
assert_eq!(html::create_metadata_tag(""), "");
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::html;
|
||||
use crate::opts::Options;
|
||||
use monolith::html;
|
||||
use monolith::opts::Options;
|
||||
use monolith::url::EMPTY_IMAGE_DATA_URL;
|
||||
|
||||
#[test]
|
||||
fn small_medium_large() {
|
||||
@@ -21,15 +23,20 @@ mod passing {
|
||||
let mut options = Options::default();
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
let embedded_css = html::embed_srcset(cache, &client, "", &srcset_value, &options, 0);
|
||||
let embedded_css = html::embed_srcset(
|
||||
cache,
|
||||
&client,
|
||||
&Url::parse("data:,").unwrap(),
|
||||
&srcset_value,
|
||||
&options,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
embedded_css,
|
||||
format!(
|
||||
"{} 1x, {} 1.5x, {} 2x",
|
||||
empty_image!(),
|
||||
empty_image!(),
|
||||
empty_image!(),
|
||||
EMPTY_IMAGE_DATA_URL, EMPTY_IMAGE_DATA_URL, EMPTY_IMAGE_DATA_URL,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -42,11 +49,18 @@ mod passing {
|
||||
let mut options = Options::default();
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
let embedded_css = html::embed_srcset(cache, &client, "", &srcset_value, &options, 0);
|
||||
let embedded_css = html::embed_srcset(
|
||||
cache,
|
||||
&client,
|
||||
&Url::parse("data:,").unwrap(),
|
||||
&srcset_value,
|
||||
&options,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
embedded_css,
|
||||
format!("{}, {} 1.5x", empty_image!(), empty_image!()),
|
||||
format!("{}, {} 1.5x", EMPTY_IMAGE_DATA_URL, EMPTY_IMAGE_DATA_URL),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,11 +72,18 @@ mod passing {
|
||||
let mut options = Options::default();
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
let embedded_css = html::embed_srcset(cache, &client, "", &srcset_value, &options, 0);
|
||||
let embedded_css = html::embed_srcset(
|
||||
cache,
|
||||
&client,
|
||||
&Url::parse("data:,").unwrap(),
|
||||
&srcset_value,
|
||||
&options,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
embedded_css,
|
||||
format!("{} 1x, {} 2x", empty_image!(), empty_image!()),
|
||||
format!("{} 1x, {} 2x", EMPTY_IMAGE_DATA_URL, EMPTY_IMAGE_DATA_URL),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,15 +95,20 @@ mod passing {
|
||||
let mut options = Options::default();
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
let embedded_css = html::embed_srcset(cache, &client, "", &srcset_value, &options, 0);
|
||||
let embedded_css = html::embed_srcset(
|
||||
cache,
|
||||
&client,
|
||||
&Url::parse("data:,").unwrap(),
|
||||
&srcset_value,
|
||||
&options,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
embedded_css,
|
||||
format!(
|
||||
"{} 1x, {} 2x, {} 3x",
|
||||
empty_image!(),
|
||||
empty_image!(),
|
||||
empty_image!()
|
||||
EMPTY_IMAGE_DATA_URL, EMPTY_IMAGE_DATA_URL, EMPTY_IMAGE_DATA_URL
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -98,10 +124,12 @@ mod passing {
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::html;
|
||||
use crate::opts::Options;
|
||||
use monolith::html;
|
||||
use monolith::opts::Options;
|
||||
use monolith::url::EMPTY_IMAGE_DATA_URL;
|
||||
|
||||
#[test]
|
||||
fn trailing_comma() {
|
||||
@@ -111,11 +139,18 @@ mod failing {
|
||||
let mut options = Options::default();
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
let embedded_css = html::embed_srcset(cache, &client, "", &srcset_value, &options, 0);
|
||||
let embedded_css = html::embed_srcset(
|
||||
cache,
|
||||
&client,
|
||||
&Url::parse("data:,").unwrap(),
|
||||
&srcset_value,
|
||||
&options,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
embedded_css,
|
||||
format!("{} 1x, {} 2x,", empty_image!(), empty_image!()),
|
||||
format!("{} 1x, {} 2x,", EMPTY_IMAGE_DATA_URL, EMPTY_IMAGE_DATA_URL),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn present() {
|
||||
@@ -19,11 +19,11 @@ mod passing {
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(
|
||||
html::get_base_url(&dom.document),
|
||||
Some(str!("https://musicbrainz.org"))
|
||||
Some("https://musicbrainz.org".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ mod passing {
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(
|
||||
html::get_base_url(&dom.document),
|
||||
Some(str!("https://www.discogs.com/"))
|
||||
Some("https://www.discogs.com/".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn absent() {
|
||||
@@ -67,7 +67,7 @@ mod failing {
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(html::get_base_url(&dom.document), None);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ mod failing {
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(html::get_base_url(&dom.document), None);
|
||||
}
|
||||
@@ -97,8 +97,8 @@ mod failing {
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(html::get_base_url(&dom.document), Some(str!()));
|
||||
assert_eq!(html::get_base_url(&dom.document), Some("".to_string()));
|
||||
}
|
||||
}
|
||||
72
tests/html/get_charset.rs
Normal file
72
tests/html/get_charset.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn meta_content_type() {
|
||||
let html = "<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=\"content-type\" content=\"text/html;charset=GB2312\" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(html::get_charset(&dom.document), Some("GB2312".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_charset() {
|
||||
let html = "<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\"GB2312\" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(html::get_charset(&dom.document), Some("GB2312".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_conflicting_meta_charset_first() {
|
||||
let html = "<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=\"utf-8\" />
|
||||
<meta http-equiv=\"content-type\" content=\"text/html;charset=GB2312\" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(html::get_charset(&dom.document), Some("utf-8".to_string()));
|
||||
}
|
||||
#[test]
|
||||
fn multiple_conflicting_meta_content_type_first() {
|
||||
let html = "<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=\"content-type\" content=\"text/html;charset=GB2312\" />
|
||||
<meta charset=\"utf-8\" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
|
||||
assert_eq!(html::get_charset(&dom.document), Some("GB2312".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@
|
||||
mod passing {
|
||||
use html5ever::rcdom::{Handle, NodeData};
|
||||
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn div_two_style_attributes() {
|
||||
let html = "<!doctype html><html><head></head><body><DIV STYLE=\"color: blue;\" style=\"display: none;\"></div></body></html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut count = 0;
|
||||
|
||||
fn test_walk(node: &Handle, i: &mut i8) {
|
||||
@@ -35,7 +35,7 @@ mod passing {
|
||||
} else if node_name == "div" {
|
||||
assert_eq!(
|
||||
html::get_node_attr(node, "style"),
|
||||
Some(str!("color: blue;"))
|
||||
Some("color: blue;".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
mod passing {
|
||||
use html5ever::rcdom::{Handle, NodeData};
|
||||
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn parent_node_names() {
|
||||
let html = "<!doctype html><html><HEAD></HEAD><body><div><P></P></div></body></html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut count = 0;
|
||||
|
||||
fn test_walk(node: &Handle, i: &mut i8) {
|
||||
@@ -7,12 +7,12 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn icon() {
|
||||
let html = "<link rel=\"icon\" href=\"\" /><div>text</div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let res: bool = html::has_favicon(&dom.document);
|
||||
|
||||
assert!(res);
|
||||
@@ -21,7 +21,7 @@ mod passing {
|
||||
#[test]
|
||||
fn shortcut_icon() {
|
||||
let html = "<link rel=\"shortcut icon\" href=\"\" /><div>text</div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let res: bool = html::has_favicon(&dom.document);
|
||||
|
||||
assert!(res);
|
||||
@@ -37,12 +37,12 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn absent() {
|
||||
let html = "<div>text</div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let res: bool = html::has_favicon(&dom.document);
|
||||
|
||||
assert!(!res);
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn icon() {
|
||||
@@ -34,7 +34,7 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn mask_icon() {
|
||||
@@ -4,10 +4,11 @@ mod compose_csp;
|
||||
mod create_metadata_tag;
|
||||
mod embed_srcset;
|
||||
mod get_base_url;
|
||||
mod get_charset;
|
||||
mod get_node_attr;
|
||||
mod get_node_name;
|
||||
mod has_favicon;
|
||||
mod is_icon;
|
||||
mod serialize_document;
|
||||
mod set_node_attr;
|
||||
mod stringify_document;
|
||||
mod walk_and_embed_assets;
|
||||
@@ -7,17 +7,17 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::html;
|
||||
use crate::opts::Options;
|
||||
use monolith::html;
|
||||
use monolith::opts::Options;
|
||||
|
||||
#[test]
|
||||
fn div_as_root_element() {
|
||||
let html = "<div><script src=\"some.js\"></script></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let options = Options::default();
|
||||
|
||||
assert_eq!(
|
||||
html::stringify_document(&dom.document, &options),
|
||||
String::from_utf8_lossy(&html::serialize_document(dom, "".to_string(), &options)),
|
||||
"<html><head></head><body><div><script src=\"some.js\"></script></div></body></html>"
|
||||
);
|
||||
}
|
||||
@@ -28,18 +28,19 @@ mod passing {
|
||||
<link rel=\"something\" href=\"some.css\" />\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src https:\">\
|
||||
<div><script src=\"some.js\"></script></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut options = Options::default();
|
||||
options.isolate = true;
|
||||
|
||||
assert_eq!(
|
||||
html::stringify_document(
|
||||
&dom.document,
|
||||
String::from_utf8_lossy(&html::serialize_document(
|
||||
dom,
|
||||
"".to_string(),
|
||||
&options
|
||||
),
|
||||
)),
|
||||
"<html>\
|
||||
<head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-inline' data:;\"></meta>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-eval' 'unsafe-inline' data:;\"></meta>\
|
||||
<title>Isolated document</title>\
|
||||
<link rel=\"something\" href=\"some.css\">\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src https:\">\
|
||||
@@ -59,12 +60,12 @@ mod passing {
|
||||
<title>Unstyled document</title>\
|
||||
<link rel=\"stylesheet\" href=\"main.css\"/>\
|
||||
<div style=\"display: none;\"></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut options = Options::default();
|
||||
options.no_css = true;
|
||||
|
||||
assert_eq!(
|
||||
html::stringify_document(&dom.document, &options),
|
||||
String::from_utf8_lossy(&html::serialize_document(dom, "".to_string(), &options)),
|
||||
"<!DOCTYPE html>\
|
||||
<html>\
|
||||
<head>\
|
||||
@@ -83,15 +84,16 @@ mod passing {
|
||||
<title>Frameless document</title>\
|
||||
<link rel=\"something\"/>\
|
||||
<div><script src=\"some.js\"></script></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut options = Options::default();
|
||||
options.no_frames = true;
|
||||
|
||||
assert_eq!(
|
||||
html::stringify_document(
|
||||
&dom.document,
|
||||
String::from_utf8_lossy(&html::serialize_document(
|
||||
dom,
|
||||
"".to_string(),
|
||||
&options
|
||||
),
|
||||
)),
|
||||
"<!DOCTYPE html>\
|
||||
<html>\
|
||||
<head>\
|
||||
@@ -115,7 +117,7 @@ mod passing {
|
||||
<img style=\"width: 100%;\" src=\"some.png\" />\
|
||||
<iframe src=\"some.html\"></iframe>\
|
||||
</div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut options = Options::default();
|
||||
options.isolate = true;
|
||||
options.no_css = true;
|
||||
@@ -125,14 +127,15 @@ mod passing {
|
||||
options.no_images = true;
|
||||
|
||||
assert_eq!(
|
||||
html::stringify_document(
|
||||
&dom.document,
|
||||
String::from_utf8_lossy(&html::serialize_document(
|
||||
dom,
|
||||
"".to_string(),
|
||||
&options
|
||||
),
|
||||
)),
|
||||
"<!DOCTYPE html>\
|
||||
<html>\
|
||||
<head>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-inline' data:; style-src 'none'; font-src 'none'; frame-src 'none'; child-src 'none'; script-src 'none'; img-src data:;\"></meta>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'unsafe-eval' 'unsafe-inline' data:; style-src 'none'; font-src 'none'; frame-src 'none'; child-src 'none'; script-src 'none'; img-src data:;\"></meta>\
|
||||
<title>no-frame no-css no-js no-image isolated document</title>\
|
||||
<meta http-equiv=\"Content-Security-Policy\" content=\"default-src https:\">\
|
||||
<link rel=\"stylesheet\" href=\"some.css\">\
|
||||
@@ -9,12 +9,12 @@
|
||||
mod passing {
|
||||
use html5ever::rcdom::{Handle, NodeData};
|
||||
|
||||
use crate::html;
|
||||
use monolith::html;
|
||||
|
||||
#[test]
|
||||
fn html_lang_and_body_style() {
|
||||
let html = "<!doctype html><html lang=\"en\"><head></head><body></body></html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut count = 0;
|
||||
|
||||
fn test_walk(node: &Handle, i: &mut i8) {
|
||||
@@ -31,23 +31,23 @@ mod passing {
|
||||
let node_name = name.local.as_ref().to_string();
|
||||
|
||||
if node_name == "html" {
|
||||
assert_eq!(html::get_node_attr(node, "lang"), Some(str!("en")));
|
||||
assert_eq!(html::get_node_attr(node, "lang"), Some("en".to_string()));
|
||||
|
||||
html::set_node_attr(node, "lang", Some(str!("de")));
|
||||
assert_eq!(html::get_node_attr(node, "lang"), Some(str!("de")));
|
||||
html::set_node_attr(node, "lang", Some("de".to_string()));
|
||||
assert_eq!(html::get_node_attr(node, "lang"), Some("de".to_string()));
|
||||
|
||||
html::set_node_attr(node, "lang", None);
|
||||
assert_eq!(html::get_node_attr(node, "lang"), None);
|
||||
|
||||
html::set_node_attr(node, "lang", Some(str!("")));
|
||||
assert_eq!(html::get_node_attr(node, "lang"), Some(str!("")));
|
||||
html::set_node_attr(node, "lang", Some("".to_string()));
|
||||
assert_eq!(html::get_node_attr(node, "lang"), Some("".to_string()));
|
||||
} else if node_name == "body" {
|
||||
assert_eq!(html::get_node_attr(node, "style"), None);
|
||||
|
||||
html::set_node_attr(node, "style", Some(str!("display: none;")));
|
||||
html::set_node_attr(node, "style", Some("display: none;".to_string()));
|
||||
assert_eq!(
|
||||
html::get_node_attr(node, "style"),
|
||||
Some(str!("display: none;"))
|
||||
Some("display: none;".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ mod passing {
|
||||
#[test]
|
||||
fn body_background() {
|
||||
let html = "<!doctype html><html lang=\"en\"><head></head><body background=\"1\" background=\"2\"></body></html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let mut count = 0;
|
||||
|
||||
fn test_walk(node: &Handle, i: &mut i8) {
|
||||
@@ -84,7 +84,10 @@ mod passing {
|
||||
let node_name = name.local.as_ref().to_string();
|
||||
|
||||
if node_name == "body" {
|
||||
assert_eq!(html::get_node_attr(node, "background"), Some(str!("1")));
|
||||
assert_eq!(
|
||||
html::get_node_attr(node, "background"),
|
||||
Some("1".to_string())
|
||||
);
|
||||
|
||||
html::set_node_attr(node, "background", None);
|
||||
assert_eq!(html::get_node_attr(node, "background"), None);
|
||||
@@ -10,17 +10,19 @@ mod passing {
|
||||
use html5ever::serialize::{serialize, SerializeOpts};
|
||||
use reqwest::blocking::Client;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
use crate::html;
|
||||
use crate::opts::Options;
|
||||
use monolith::html;
|
||||
use monolith::opts::Options;
|
||||
use monolith::url::EMPTY_IMAGE_DATA_URL;
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let html = "<div><P></P></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let html: &str = "<div><P></P></div>";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
@@ -41,8 +43,8 @@ mod passing {
|
||||
#[test]
|
||||
fn ensure_no_recursive_iframe() {
|
||||
let html = "<div><P></P><iframe src=\"\"></iframe></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -64,8 +66,8 @@ mod passing {
|
||||
#[test]
|
||||
fn ensure_no_recursive_frame() {
|
||||
let html = "<frameset><frame src=\"\"></frameset>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -86,12 +88,14 @@ mod passing {
|
||||
|
||||
#[test]
|
||||
fn no_css() {
|
||||
let html = "<link rel=\"stylesheet\" href=\"main.css\">\
|
||||
<link rel=\"alternate stylesheet\" href=\"main.css\">\
|
||||
<style>html{background-color: #000;}</style>\
|
||||
<div style=\"display: none;\"></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let html = "\
|
||||
<link rel=\"stylesheet\" href=\"main.css\">\
|
||||
<link rel=\"alternate stylesheet\" href=\"main.css\">\
|
||||
<style>html{background-color: #000;}</style>\
|
||||
<div style=\"display: none;\"></div>\
|
||||
";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -107,16 +111,18 @@ mod passing {
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"<html>\
|
||||
<head>\
|
||||
<link rel=\"stylesheet\">\
|
||||
<link rel=\"alternate stylesheet\">\
|
||||
<style></style>\
|
||||
</head>\
|
||||
<body>\
|
||||
<div></div>\
|
||||
</body>\
|
||||
</html>"
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
<link rel=\"stylesheet\">\
|
||||
<link rel=\"alternate stylesheet\">\
|
||||
<style></style>\
|
||||
</head>\
|
||||
<body>\
|
||||
<div></div>\
|
||||
</body>\
|
||||
</html>\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,8 +130,8 @@ mod passing {
|
||||
fn no_images() {
|
||||
let html = "<link rel=\"icon\" href=\"favicon.ico\">\
|
||||
<div><img src=\"http://localhost/assets/mono_lisa.png\" /></div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -152,7 +158,7 @@ mod passing {
|
||||
</div>\
|
||||
</body>\
|
||||
</html>",
|
||||
empty_image = empty_image!()
|
||||
empty_image = EMPTY_IMAGE_DATA_URL
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -161,8 +167,8 @@ mod passing {
|
||||
fn no_body_background_images() {
|
||||
let html =
|
||||
"<body background=\"no/such/image.png\" background=\"no/such/image2.png\"></body>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -185,8 +191,8 @@ mod passing {
|
||||
#[test]
|
||||
fn no_frames() {
|
||||
let html = "<frameset><frame src=\"http://trackbook.com\"></frameset>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -202,15 +208,23 @@ mod passing {
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"<html><head></head><frameset><frame src=\"\"></frameset></html>"
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
</head>\
|
||||
<frameset>\
|
||||
<frame src=\"\">\
|
||||
</frameset>\
|
||||
</html>\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_iframes() {
|
||||
let html = "<iframe src=\"http://trackbook.com\"></iframe>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -226,18 +240,27 @@ mod passing {
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"<html><head></head><body><iframe src=\"\"></iframe></body></html>"
|
||||
"\
|
||||
<html>\
|
||||
<head></head>\
|
||||
<body>\
|
||||
<iframe src=\"\"></iframe>\
|
||||
</body>\
|
||||
</html>\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_js() {
|
||||
let html = "<div onClick=\"void(0)\">\
|
||||
<script src=\"http://localhost/assets/some.js\"></script>\
|
||||
<script>alert(1)</script>\
|
||||
</div>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let html = "\
|
||||
<div onClick=\"void(0)\">\
|
||||
<script src=\"http://localhost/assets/some.js\"></script>\
|
||||
<script>alert(1)</script>\
|
||||
</div>\
|
||||
";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -253,25 +276,29 @@ mod passing {
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"<html><head></head><body><div><script></script>\
|
||||
<script></script></div></body></html>"
|
||||
"\
|
||||
<html>\
|
||||
<head></head>\
|
||||
<body>\
|
||||
<div>\
|
||||
<script></script>\
|
||||
<script></script>\
|
||||
</div>\
|
||||
</body>\
|
||||
</html>\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discards_integrity() {
|
||||
let html = "<title>No integrity</title>\
|
||||
<link integrity=\"sha384-...\" rel=\"something\"/>\
|
||||
<script integrity=\"sha384-...\" src=\"some.js\"></script>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
fn keeps_integrity_for_unfamiliar_links() {
|
||||
let html = "<title>Has integrity</title>\
|
||||
<link integrity=\"sha384-12345\" rel=\"something\" href=\"https://some-site.com/some-file.ext\" />";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
options.no_css = true;
|
||||
options.no_frames = true;
|
||||
options.no_js = true;
|
||||
options.no_images = true;
|
||||
options.silent = true;
|
||||
|
||||
let client = Client::new();
|
||||
@@ -283,24 +310,109 @@ mod passing {
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"<html>\
|
||||
<head><title>No integrity</title><link rel=\"something\"><script></script></head>\
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
<title>Has integrity</title>\
|
||||
<link integrity=\"sha384-12345\" rel=\"something\" href=\"https://some-site.com/some-file.ext\">\
|
||||
</head>\
|
||||
<body></body>\
|
||||
</html>"
|
||||
</html>\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discards_integrity_for_known_links_nojs_nocss() {
|
||||
let html = "\
|
||||
<title>No integrity</title>\
|
||||
<link integrity=\"\" rel=\"stylesheet\" href=\"data:;\"/>\
|
||||
<script integrity=\"\" src=\"some.js\"></script>\
|
||||
";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
options.no_css = true;
|
||||
options.no_js = true;
|
||||
options.silent = true;
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
html::walk_and_embed_assets(cache, &client, &url, &dom.document, &options, 0);
|
||||
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
serialize(&mut buf, &dom.document, SerializeOpts::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
<title>No integrity</title>\
|
||||
<link rel=\"stylesheet\">\
|
||||
<script></script>\
|
||||
</head>\
|
||||
<body></body>\
|
||||
</html>\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discards_integrity_for_embedded_assets() {
|
||||
let html = "\
|
||||
<title>No integrity</title>\
|
||||
<link integrity=\"sha384-123\" rel=\"something\" href=\"data:;\"/>\
|
||||
<script integrity=\"sha384-456\" src=\"some.js\"></script>\
|
||||
";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
options.no_css = true;
|
||||
options.no_js = true;
|
||||
options.silent = true;
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
html::walk_and_embed_assets(cache, &client, &url, &dom.document, &options, 0);
|
||||
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
serialize(&mut buf, &dom.document, SerializeOpts::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
<title>No integrity</title>\
|
||||
<link integrity=\"sha384-123\" rel=\"something\" href=\"data:;\">\
|
||||
<script></script>\
|
||||
</head>\
|
||||
<body>\
|
||||
</body>\
|
||||
</html>\
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_unwanted_meta_tags() {
|
||||
let html = "<html>\
|
||||
<head>\
|
||||
<meta http-equiv=\"Refresh\" value=\"20\"/>\
|
||||
<meta http-equiv=\"Location\" value=\"https://freebsd.org\"/>\
|
||||
</head>\
|
||||
<body></body>\
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let html = "\
|
||||
<html>\
|
||||
<head>\
|
||||
<meta http-equiv=\"Refresh\" content=\"2\"/>\
|
||||
<meta http-equiv=\"Location\" content=\"https://freebsd.org\"/>\
|
||||
</head>\
|
||||
<body>\
|
||||
</body>\
|
||||
</html>\
|
||||
";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -319,27 +431,30 @@ mod passing {
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"<html>\
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
<meta http-equiv=\"disabled by monolith (Refresh)\" value=\"20\">\
|
||||
<meta http-equiv=\"disabled by monolith (Location)\" value=\"https://freebsd.org\">\
|
||||
<meta content=\"2\">\
|
||||
<meta content=\"https://freebsd.org\">\
|
||||
</head>\
|
||||
<body></body>\
|
||||
<body>\
|
||||
</body>\
|
||||
</html>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn processes_noscript_tags() {
|
||||
let html = "<html>\
|
||||
let html = "\
|
||||
<html>\
|
||||
<body>\
|
||||
<noscript>\
|
||||
<img src=\"image.png\" />\
|
||||
</noscript>\
|
||||
</body>\
|
||||
</html>";
|
||||
let dom = html::html_to_dom(&html);
|
||||
let url = "http://localhost";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
@@ -356,7 +471,8 @@ mod passing {
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
format!(
|
||||
"<html>\
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
</head>\
|
||||
<body>\
|
||||
@@ -365,8 +481,38 @@ mod passing {
|
||||
</noscript>\
|
||||
</body>\
|
||||
</html>",
|
||||
empty_image!(),
|
||||
EMPTY_IMAGE_DATA_URL,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_script_type_json() {
|
||||
let html = "<script id=\"data\" type=\"application/json\">{\"mono\":\"lith\"}</script>";
|
||||
let dom = html::html_to_dom(&html.as_bytes().to_vec(), "".to_string());
|
||||
let url: Url = Url::parse("http://localhost").unwrap();
|
||||
let cache = &mut HashMap::new();
|
||||
|
||||
let mut options = Options::default();
|
||||
options.silent = true;
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
html::walk_and_embed_assets(cache, &client, &url, &dom.document, &options, 0);
|
||||
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
serialize(&mut buf, &dom.document, SerializeOpts::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buf.iter().map(|&c| c as char).collect::<String>(),
|
||||
"\
|
||||
<html>\
|
||||
<head>\
|
||||
<script id=\"data\" type=\"application/json\">{\"mono\":\"lith\"}</script>\
|
||||
</head>\
|
||||
<body>\
|
||||
</body>\
|
||||
</html>"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::js;
|
||||
use monolith::js;
|
||||
|
||||
#[test]
|
||||
fn onblur_camelcase() {
|
||||
@@ -34,7 +34,7 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::js;
|
||||
use monolith::js;
|
||||
|
||||
#[test]
|
||||
fn href() {
|
||||
@@ -2,7 +2,7 @@ mod cli;
|
||||
mod css;
|
||||
mod html;
|
||||
mod js;
|
||||
mod macros;
|
||||
// mod macros;
|
||||
mod opts;
|
||||
mod url;
|
||||
mod utils;
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::opts::Options;
|
||||
use monolith::opts::Options;
|
||||
|
||||
#[test]
|
||||
fn defaults() {
|
||||
@@ -16,6 +16,7 @@ mod passing {
|
||||
assert_eq!(options.no_audio, false);
|
||||
assert_eq!(options.base_url, None);
|
||||
assert_eq!(options.no_css, false);
|
||||
assert_eq!(options.charset, None);
|
||||
assert_eq!(options.no_frames, false);
|
||||
assert_eq!(options.no_fonts, false);
|
||||
assert_eq!(options.no_images, false);
|
||||
@@ -23,12 +24,12 @@ mod passing {
|
||||
assert_eq!(options.no_js, false);
|
||||
assert_eq!(options.insecure, false);
|
||||
assert_eq!(options.no_metadata, false);
|
||||
assert_eq!(options.output, str!());
|
||||
assert_eq!(options.output, "".to_string());
|
||||
assert_eq!(options.silent, false);
|
||||
assert_eq!(options.timeout, 0);
|
||||
assert_eq!(options.user_agent, None);
|
||||
assert_eq!(options.no_video, false);
|
||||
|
||||
assert_eq!(options.target, str!());
|
||||
assert_eq!(options.target, "".to_string());
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,24 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn preserve_original() {
|
||||
let u: Url = Url::parse("https://somewhere.com/font.eot#iefix").unwrap();
|
||||
|
||||
let clean_u: Url = url::clean_url(u.clone());
|
||||
|
||||
assert_eq!(clean_u.as_str(), "https://somewhere.com/font.eot");
|
||||
assert_eq!(u.as_str(), "https://somewhere.com/font.eot#iefix");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_fragment() {
|
||||
assert_eq!(
|
||||
url::clean_url("https://somewhere.com/font.eot#iefix"),
|
||||
url::clean_url(Url::parse("https://somewhere.com/font.eot#iefix").unwrap()).as_str(),
|
||||
"https://somewhere.com/font.eot"
|
||||
);
|
||||
}
|
||||
@@ -20,31 +32,31 @@ mod passing {
|
||||
#[test]
|
||||
fn removes_empty_fragment() {
|
||||
assert_eq!(
|
||||
url::clean_url("https://somewhere.com/font.eot#"),
|
||||
url::clean_url(Url::parse("https://somewhere.com/font.eot#").unwrap()).as_str(),
|
||||
"https://somewhere.com/font.eot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_empty_query_and_empty_fragment() {
|
||||
fn removes_empty_fragment_and_keeps_empty_query() {
|
||||
assert_eq!(
|
||||
url::clean_url("https://somewhere.com/font.eot?#"),
|
||||
"https://somewhere.com/font.eot"
|
||||
url::clean_url(Url::parse("https://somewhere.com/font.eot?#").unwrap()).as_str(),
|
||||
"https://somewhere.com/font.eot?"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removes_empty_query_amp_and_empty_fragment() {
|
||||
fn removesempty_fragment_and_keeps_empty_query() {
|
||||
assert_eq!(
|
||||
url::clean_url("https://somewhere.com/font.eot?a=b&#"),
|
||||
"https://somewhere.com/font.eot?a=b"
|
||||
url::clean_url(Url::parse("https://somewhere.com/font.eot?a=b&#").unwrap()).as_str(),
|
||||
"https://somewhere.com/font.eot?a=b&"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_credentials() {
|
||||
assert_eq!(
|
||||
url::clean_url("https://cookie:monster@gibson.internet/"),
|
||||
url::clean_url(Url::parse("https://cookie:monster@gibson.internet/").unwrap()).as_str(),
|
||||
"https://cookie:monster@gibson.internet/"
|
||||
);
|
||||
}
|
||||
109
tests/url/create_data_url.rs
Normal file
109
tests/url/create_data_url.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn encode_string_with_specific_media_type() {
|
||||
let media_type = "application/javascript";
|
||||
let data = "var word = 'hello';\nalert(word);\n";
|
||||
let data_url = url::create_data_url(
|
||||
media_type,
|
||||
"",
|
||||
data.as_bytes(),
|
||||
&Url::parse("data:,").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
data_url.as_str(),
|
||||
"data:application/javascript;base64,dmFyIHdvcmQgPSAnaGVsbG8nOwphbGVydCh3b3JkKTsK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_append_fragment() {
|
||||
let data = "<svg></svg>\n";
|
||||
let data_url = url::create_data_url(
|
||||
"image/svg+xml",
|
||||
"",
|
||||
data.as_bytes(),
|
||||
&Url::parse("data:,").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
data_url.as_str(),
|
||||
"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4K"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_string_with_specific_media_type_and_charset() {
|
||||
let media_type = "application/javascript";
|
||||
let charset = "utf8";
|
||||
let data = "var word = 'hello';\nalert(word);\n";
|
||||
let data_url = url::create_data_url(
|
||||
media_type,
|
||||
charset,
|
||||
data.as_bytes(),
|
||||
&Url::parse("data:,").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
data_url.as_str(),
|
||||
"data:application/javascript;charset=utf8;base64,dmFyIHdvcmQgPSAnaGVsbG8nOwphbGVydCh3b3JkKTsK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_data_url_with_us_ascii_charset() {
|
||||
let media_type = "";
|
||||
let charset = "us-ascii";
|
||||
let data = "";
|
||||
let data_url = url::create_data_url(
|
||||
media_type,
|
||||
charset,
|
||||
data.as_bytes(),
|
||||
&Url::parse("data:,").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(data_url.as_str(), "data:;base64,");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_data_url_with_utf8_charset() {
|
||||
let media_type = "";
|
||||
let charset = "utf8";
|
||||
let data = "";
|
||||
let data_url = url::create_data_url(
|
||||
media_type,
|
||||
charset,
|
||||
data.as_bytes(),
|
||||
&Url::parse("data:,").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(data_url.as_str(), "data:;charset=utf8;base64,");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_data_url_with_media_type_text_plain_and_utf8_charset() {
|
||||
let media_type = "text/plain";
|
||||
let charset = "utf8";
|
||||
let data = "";
|
||||
let data_url = url::create_data_url(
|
||||
media_type,
|
||||
charset,
|
||||
data.as_bytes(),
|
||||
&Url::parse("data:,").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(data_url.as_str(), "data:text/plain;charset=utf8;base64,");
|
||||
}
|
||||
}
|
||||
@@ -7,57 +7,67 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn mailto() {
|
||||
assert!(url::url_has_protocol(
|
||||
assert!(url::is_url_and_has_protocol(
|
||||
"mailto:somebody@somewhere.com?subject=hello"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tel() {
|
||||
assert!(url::url_has_protocol("tel:5551234567"));
|
||||
assert!(url::is_url_and_has_protocol("tel:5551234567"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftp_no_slashes() {
|
||||
assert!(url::url_has_protocol("ftp:some-ftp-server.com"));
|
||||
assert!(url::is_url_and_has_protocol("ftp:some-ftp-server.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftp_with_credentials() {
|
||||
assert!(url::url_has_protocol(
|
||||
assert!(url::is_url_and_has_protocol(
|
||||
"ftp://user:password@some-ftp-server.com"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript() {
|
||||
assert!(url::url_has_protocol("javascript:void(0)"));
|
||||
assert!(url::is_url_and_has_protocol("javascript:void(0)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http() {
|
||||
assert!(url::url_has_protocol("http://news.ycombinator.com"));
|
||||
assert!(url::is_url_and_has_protocol("http://news.ycombinator.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https() {
|
||||
assert!(url::url_has_protocol("https://github.com"));
|
||||
assert!(url::is_url_and_has_protocol("https://github.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file() {
|
||||
assert!(url::is_url_and_has_protocol("file:///tmp/image.png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailto_uppercase() {
|
||||
assert!(url::url_has_protocol(
|
||||
assert!(url::is_url_and_has_protocol(
|
||||
"MAILTO:somebody@somewhere.com?subject=hello"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_data_url() {
|
||||
assert!(url::url_has_protocol("data:text/html,"));
|
||||
assert!(url::is_url_and_has_protocol("data:text/html,"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_data_url_surrounded_by_spaces() {
|
||||
assert!(url::is_url_and_has_protocol(" data:text/html, "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,25 +80,31 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::url;
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn url_with_no_protocol() {
|
||||
assert!(!url::url_has_protocol("//some-hostname.com/some-file.html"));
|
||||
assert_eq!(
|
||||
url::is_url_and_has_protocol("//some-hostname.com/some-file.html"),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_path() {
|
||||
assert!(!url::url_has_protocol("some-hostname.com/some-file.html"));
|
||||
assert_eq!(
|
||||
url::is_url_and_has_protocol("some-hostname.com/some-file.html"),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_to_root_path() {
|
||||
assert!(!url::url_has_protocol("/some-file.html"));
|
||||
assert_eq!(url::is_url_and_has_protocol("/some-file.html"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_string() {
|
||||
assert!(!url::url_has_protocol(""));
|
||||
assert_eq!(url::is_url_and_has_protocol(""), false);
|
||||
}
|
||||
}
|
||||
5
tests/url/mod.rs
Normal file
5
tests/url/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod clean_url;
|
||||
mod create_data_url;
|
||||
mod is_url_and_has_protocol;
|
||||
mod parse_data_url;
|
||||
mod resolve_url;
|
||||
@@ -7,13 +7,16 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn parse_text_html_base64() {
|
||||
let (media_type, data) = url::parse_data_url("data:text/html;base64,V29yayBleHBhbmRzIHNvIGFzIHRvIGZpbGwgdGhlIHRpbWUgYXZhaWxhYmxlIGZvciBpdHMgY29tcGxldGlvbg==");
|
||||
let (media_type, charset, data) = url::parse_data_url(&Url::parse("data:text/html;base64,V29yayBleHBhbmRzIHNvIGFzIHRvIGZpbGwgdGhlIHRpbWUgYXZhaWxhYmxlIGZvciBpdHMgY29tcGxldGlvbg==").unwrap());
|
||||
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&data),
|
||||
"Work expands so as to fill the time available for its completion"
|
||||
@@ -22,11 +25,12 @@ mod passing {
|
||||
|
||||
#[test]
|
||||
fn parse_text_html_utf8() {
|
||||
let (media_type, data) = url::parse_data_url(
|
||||
"data:text/html;utf8,Work expands so as to fill the time available for its completion",
|
||||
let (media_type, charset, data) = url::parse_data_url(
|
||||
&Url::parse("data:text/html;charset=utf8,Work expands so as to fill the time available for its completion").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "utf8");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&data),
|
||||
"Work expands so as to fill the time available for its completion"
|
||||
@@ -35,22 +39,15 @@ mod passing {
|
||||
|
||||
#[test]
|
||||
fn parse_text_html_plaintext() {
|
||||
let (media_type, data) = url::parse_data_url(
|
||||
"data:text/html,Work expands so as to fill the time available for its completion",
|
||||
let (media_type, charset, data) = url::parse_data_url(
|
||||
&Url::parse(
|
||||
"data:text/html,Work expands so as to fill the time available for its completion",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&data),
|
||||
"Work expands so as to fill the time available for its completion"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_text_html_charset_utf_8_between_two_whitespaces() {
|
||||
let (media_type, data) = url::parse_data_url(" data:text/html;charset=utf-8,Work expands so as to fill the time available for its completion ");
|
||||
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&data),
|
||||
"Work expands so as to fill the time available for its completion"
|
||||
@@ -59,25 +56,31 @@ mod passing {
|
||||
|
||||
#[test]
|
||||
fn parse_text_css_url_encoded() {
|
||||
let (media_type, data) = url::parse_data_url("data:text/css,div{background-color:%23000}");
|
||||
let (media_type, charset, data) =
|
||||
url::parse_data_url(&Url::parse("data:text/css,div{background-color:%23000}").unwrap());
|
||||
|
||||
assert_eq!(media_type, "text/css");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert_eq!(String::from_utf8_lossy(&data), "div{background-color:#000}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_media_type_base64() {
|
||||
let (media_type, data) = url::parse_data_url("data:;base64,dGVzdA==");
|
||||
let (media_type, charset, data) =
|
||||
url::parse_data_url(&Url::parse("data:;base64,dGVzdA==").unwrap());
|
||||
|
||||
assert_eq!(media_type, "");
|
||||
assert_eq!(media_type, "text/plain");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert_eq!(String::from_utf8_lossy(&data), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_media_type_no_encoding() {
|
||||
let (media_type, data) = url::parse_data_url("data:;,test%20test");
|
||||
let (media_type, charset, data) =
|
||||
url::parse_data_url(&Url::parse("data:;,test%20test").unwrap());
|
||||
|
||||
assert_eq!(media_type, "");
|
||||
assert_eq!(media_type, "text/plain");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert_eq!(String::from_utf8_lossy(&data), "test test");
|
||||
}
|
||||
}
|
||||
@@ -91,13 +94,16 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::url;
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn just_word_data() {
|
||||
let (media_type, data) = url::parse_data_url("data");
|
||||
fn empty_data_url() {
|
||||
let (media_type, charset, data) = url::parse_data_url(&Url::parse("data:,").unwrap());
|
||||
|
||||
assert_eq!(media_type, "");
|
||||
assert_eq!(media_type, "text/plain");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert_eq!(String::from_utf8_lossy(&data), "");
|
||||
}
|
||||
}
|
||||
@@ -7,26 +7,49 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::url;
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn from_https_to_level_up_relative() {
|
||||
fn basic_httsp_relative() {
|
||||
assert_eq!(
|
||||
url::resolve_url("https://www.kernel.org", "../category/signatures.html")
|
||||
.unwrap_or_default(),
|
||||
"https://www.kernel.org/category/signatures.html"
|
||||
url::resolve_url(
|
||||
&Url::parse("https://www.kernel.org").unwrap(),
|
||||
"category/signatures.html"
|
||||
)
|
||||
.as_str(),
|
||||
Url::parse("https://www.kernel.org/category/signatures.html")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_just_filename_to_full_https_url() {
|
||||
fn basic_httsp_absolute() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"saved_page.htm",
|
||||
"https://www.kernel.org/category/signatures.html",
|
||||
&Url::parse("https://www.kernel.org").unwrap(),
|
||||
"/category/signatures.html"
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
"https://www.kernel.org/category/signatures.html"
|
||||
.as_str(),
|
||||
Url::parse("https://www.kernel.org/category/signatures.html")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_https_to_level_up_relative() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
&Url::parse("https://www.kernel.org").unwrap(),
|
||||
"../category/signatures.html"
|
||||
)
|
||||
.as_str(),
|
||||
Url::parse("https://www.kernel.org/category/signatures.html")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,10 +57,10 @@ mod passing {
|
||||
fn from_https_url_to_url_with_no_protocol() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"https://www.kernel.org",
|
||||
&Url::parse("https://www.kernel.org").unwrap(),
|
||||
"//www.kernel.org/theme/images/logos/tux.png",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"https://www.kernel.org/theme/images/logos/tux.png"
|
||||
);
|
||||
}
|
||||
@@ -46,22 +69,22 @@ mod passing {
|
||||
fn from_https_url_to_url_with_no_protocol_and_on_different_hostname() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"https://www.kernel.org",
|
||||
&Url::parse("https://www.kernel.org").unwrap(),
|
||||
"//another-host.org/theme/images/logos/tux.png",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"https://another-host.org/theme/images/logos/tux.png"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_https_url_to_relative_root_path() {
|
||||
fn from_https_url_to_absolute_path() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"https://www.kernel.org/category/signatures.html",
|
||||
&Url::parse("https://www.kernel.org/category/signatures.html").unwrap(),
|
||||
"/theme/images/logos/tux.png",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"https://www.kernel.org/theme/images/logos/tux.png"
|
||||
);
|
||||
}
|
||||
@@ -70,10 +93,10 @@ mod passing {
|
||||
fn from_https_to_just_filename() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"https://www.w3schools.com/html/html_iframe.asp",
|
||||
&Url::parse("https://www.w3schools.com/html/html_iframe.asp").unwrap(),
|
||||
"default.asp",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"https://www.w3schools.com/html/default.asp"
|
||||
);
|
||||
}
|
||||
@@ -82,10 +105,11 @@ mod passing {
|
||||
fn from_data_url_to_https() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h",
|
||||
&Url::parse("data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h")
|
||||
.unwrap(),
|
||||
"https://www.kernel.org/category/signatures.html",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"https://www.kernel.org/category/signatures.html"
|
||||
);
|
||||
}
|
||||
@@ -94,10 +118,11 @@ mod passing {
|
||||
fn from_data_url_to_data_url() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h",
|
||||
&Url::parse("data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h")
|
||||
.unwrap(),
|
||||
"data:text/html;base64,PGEgaHJlZj0iaW5kZXguaHRtbCI+SG9tZTwvYT4K",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"data:text/html;base64,PGEgaHJlZj0iaW5kZXguaHRtbCI+SG9tZTwvYT4K"
|
||||
);
|
||||
}
|
||||
@@ -106,10 +131,10 @@ mod passing {
|
||||
fn from_file_url_to_relative_path() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"file:///home/user/Websites/my-website/index.html",
|
||||
&Url::parse("file:///home/user/Websites/my-website/index.html").unwrap(),
|
||||
"assets/images/logo.png",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"file:///home/user/Websites/my-website/assets/images/logo.png"
|
||||
);
|
||||
}
|
||||
@@ -118,10 +143,10 @@ mod passing {
|
||||
fn from_file_url_to_relative_path_with_backslashes() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"file:\\\\\\home\\user\\Websites\\my-website\\index.html",
|
||||
&Url::parse("file:\\\\\\home\\user\\Websites\\my-website\\index.html").unwrap(),
|
||||
"assets\\images\\logo.png",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"file:///home/user/Websites/my-website/assets/images/logo.png"
|
||||
);
|
||||
}
|
||||
@@ -130,10 +155,11 @@ mod passing {
|
||||
fn from_data_url_to_file_url() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h",
|
||||
&Url::parse("data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h")
|
||||
.unwrap(),
|
||||
"file:///etc/passwd",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"file:///etc/passwd"
|
||||
);
|
||||
}
|
||||
@@ -142,30 +168,35 @@ mod passing {
|
||||
fn preserve_fragment() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"http://doesnt-matter.local/",
|
||||
&Url::parse("http://doesnt-matter.local/").unwrap(),
|
||||
"css/fonts/fontmarvelous.svg#fontmarvelous",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
.as_str(),
|
||||
"http://doesnt-matter.local/css/fonts/fontmarvelous.svg#fontmarvelous"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_from_file_url_to_file_url() {
|
||||
assert_eq!(
|
||||
if cfg!(windows) {
|
||||
url::resolve_url("file:///c:/index.html", "file:///c:/image.png")
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
url::resolve_url("file:///tmp/index.html", "file:///tmp/image.png")
|
||||
.unwrap_or_default()
|
||||
},
|
||||
if cfg!(windows) {
|
||||
if cfg!(windows) {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
&Url::parse("file:///c:/index.html").unwrap(),
|
||||
"file:///c:/image.png"
|
||||
)
|
||||
.as_str(),
|
||||
"file:///c:/image.png"
|
||||
} else {
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
&Url::parse("file:///tmp/index.html").unwrap(),
|
||||
"file:///tmp/image.png"
|
||||
)
|
||||
.as_str(),
|
||||
"file:///tmp/image.png"
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,17 +209,20 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::url;
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::url;
|
||||
|
||||
#[test]
|
||||
fn from_data_url_to_url_with_no_protocol() {
|
||||
assert_eq!(
|
||||
url::resolve_url(
|
||||
"data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h",
|
||||
&Url::parse("data:text/html;base64,V2VsY29tZSBUbyBUaGUgUGFydHksIDxiPlBhbDwvYj4h")
|
||||
.unwrap(),
|
||||
"//www.w3schools.com/html/html_iframe.asp",
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
""
|
||||
.as_str(),
|
||||
"data:,"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,131 +7,171 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::utils;
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::utils;
|
||||
|
||||
#[test]
|
||||
fn image_gif87() {
|
||||
assert_eq!(utils::detect_media_type(b"GIF87a", ""), "image/gif");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(utils::detect_media_type(b"GIF87a", &dummy_url), "image/gif");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_gif89() {
|
||||
assert_eq!(utils::detect_media_type(b"GIF89a", ""), "image/gif");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(utils::detect_media_type(b"GIF89a", &dummy_url), "image/gif");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_jpeg() {
|
||||
assert_eq!(utils::detect_media_type(b"\xFF\xD8\xFF", ""), "image/jpeg");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"\xFF\xD8\xFF", &dummy_url),
|
||||
"image/jpeg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_png() {
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"\x89PNG\x0D\x0A\x1A\x0A", ""),
|
||||
utils::detect_media_type(b"\x89PNG\x0D\x0A\x1A\x0A", &dummy_url),
|
||||
"image/png"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_svg() {
|
||||
assert_eq!(utils::detect_media_type(b"<svg ", ""), "image/svg+xml");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"<svg ", &dummy_url),
|
||||
"image/svg+xml"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_webp() {
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"RIFF....WEBPVP8 ", ""),
|
||||
utils::detect_media_type(b"RIFF....WEBPVP8 ", &dummy_url),
|
||||
"image/webp"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_icon() {
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"\x00\x00\x01\x00", ""),
|
||||
utils::detect_media_type(b"\x00\x00\x01\x00", &dummy_url),
|
||||
"image/x-icon"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_svg_filename() {
|
||||
let file_url: Url = Url::parse("file:///tmp/local-file.svg").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"<?xml ", "local-file.svg"),
|
||||
utils::detect_media_type(b"<?xml ", &file_url),
|
||||
"image/svg+xml"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_svg_url_uppercase() {
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"", "https://some-site.com/images/local-file.SVG"),
|
||||
"image/svg+xml"
|
||||
);
|
||||
let https_url: Url = Url::parse("https://some-site.com/images/local-file.SVG").unwrap();
|
||||
assert_eq!(utils::detect_media_type(b"", &https_url), "image/svg+xml");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_mpeg() {
|
||||
assert_eq!(utils::detect_media_type(b"ID3", ""), "audio/mpeg");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(utils::detect_media_type(b"ID3", &dummy_url), "audio/mpeg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_mpeg_2() {
|
||||
assert_eq!(utils::detect_media_type(b"\xFF\x0E", ""), "audio/mpeg");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"\xFF\x0E", &dummy_url),
|
||||
"audio/mpeg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_mpeg_3() {
|
||||
assert_eq!(utils::detect_media_type(b"\xFF\x0F", ""), "audio/mpeg");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"\xFF\x0F", &dummy_url),
|
||||
"audio/mpeg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_ogg() {
|
||||
assert_eq!(utils::detect_media_type(b"OggS", ""), "audio/ogg");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(utils::detect_media_type(b"OggS", &dummy_url), "audio/ogg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_wav() {
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"RIFF....WAVEfmt ", ""),
|
||||
utils::detect_media_type(b"RIFF....WAVEfmt ", &dummy_url),
|
||||
"audio/wav"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_flac() {
|
||||
assert_eq!(utils::detect_media_type(b"fLaC", ""), "audio/x-flac");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"fLaC", &dummy_url),
|
||||
"audio/x-flac"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_avi() {
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"RIFF....AVI LIST", ""),
|
||||
utils::detect_media_type(b"RIFF....AVI LIST", &dummy_url),
|
||||
"video/avi"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_mp4() {
|
||||
assert_eq!(utils::detect_media_type(b"....ftyp", ""), "video/mp4");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"....ftyp", &dummy_url),
|
||||
"video/mp4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_mpeg() {
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"\x00\x00\x01\x0B", ""),
|
||||
utils::detect_media_type(b"\x00\x00\x01\x0B", &dummy_url),
|
||||
"video/mpeg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_quicktime() {
|
||||
assert_eq!(utils::detect_media_type(b"....moov", ""), "video/quicktime");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"....moov", &dummy_url),
|
||||
"video/quicktime"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_webm() {
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"\x1A\x45\xDF\xA3", ""),
|
||||
utils::detect_media_type(b"\x1A\x45\xDF\xA3", &dummy_url),
|
||||
"video/webm"
|
||||
);
|
||||
}
|
||||
@@ -146,10 +186,16 @@ mod passing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use crate::utils;
|
||||
use reqwest::Url;
|
||||
|
||||
use monolith::utils;
|
||||
|
||||
#[test]
|
||||
fn unknown_media_type() {
|
||||
assert_eq!(utils::detect_media_type(b"abcdef0123456789", ""), "");
|
||||
let dummy_url: Url = Url::parse("data:,").unwrap();
|
||||
assert_eq!(
|
||||
utils::detect_media_type(b"abcdef0123456789", &dummy_url),
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use crate::utils;
|
||||
use monolith::utils;
|
||||
|
||||
#[test]
|
||||
fn zero() {
|
||||
@@ -28,4 +28,9 @@ mod passing {
|
||||
fn three() {
|
||||
assert_eq!(utils::indent(3), " ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four() {
|
||||
assert_eq!(utils::indent(4), " ");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod detect_media_type;
|
||||
mod indent;
|
||||
mod parse_content_type;
|
||||
mod retrieve_asset;
|
||||
86
tests/utils/parse_content_type.rs
Normal file
86
tests/utils/parse_content_type.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
// ██████╗ █████╗ ███████╗███████╗██╗███╗ ██╗ ██████╗
|
||||
// ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗ ██║██╔════╝
|
||||
// ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║ ███╗
|
||||
// ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║ ██║
|
||||
// ██║ ██║ ██║███████║███████║██║██║ ╚████║╚██████╔╝
|
||||
// ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝
|
||||
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use monolith::utils;
|
||||
|
||||
#[test]
|
||||
fn text_plain_utf8() {
|
||||
let (media_type, charset, is_base64) = utils::parse_content_type("text/plain;charset=utf8");
|
||||
assert_eq!(media_type, "text/plain");
|
||||
assert_eq!(charset, "utf8");
|
||||
assert!(!is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_plain_utf8_spaces() {
|
||||
let (media_type, charset, is_base64) =
|
||||
utils::parse_content_type(" text/plain ; charset=utf8 ");
|
||||
assert_eq!(media_type, "text/plain");
|
||||
assert_eq!(charset, "utf8");
|
||||
assert!(!is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let (media_type, charset, is_base64) = utils::parse_content_type("");
|
||||
assert_eq!(media_type, "text/plain");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert!(!is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64() {
|
||||
let (media_type, charset, is_base64) = utils::parse_content_type(";base64");
|
||||
assert_eq!(media_type, "text/plain");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert!(is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_html_base64() {
|
||||
let (media_type, charset, is_base64) = utils::parse_content_type("text/html;base64");
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert!(is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_media_type() {
|
||||
let (media_type, charset, is_base64) = utils::parse_content_type("text/html");
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert!(!is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_media_type_colon() {
|
||||
let (media_type, charset, is_base64) = utils::parse_content_type("text/html;");
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "US-ASCII");
|
||||
assert!(!is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_type_gb2312_filename() {
|
||||
let (media_type, charset, is_base64) =
|
||||
utils::parse_content_type("text/html;charset=GB2312;filename=index.html");
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "GB2312");
|
||||
assert!(!is_base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_type_filename_gb2312() {
|
||||
let (media_type, charset, is_base64) =
|
||||
utils::parse_content_type("text/html;filename=index.html;charset=GB2312");
|
||||
assert_eq!(media_type, "text/html");
|
||||
assert_eq!(charset, "GB2312");
|
||||
assert!(!is_base64);
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@
|
||||
#[cfg(test)]
|
||||
mod passing {
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
use crate::opts::Options;
|
||||
use crate::url;
|
||||
use crate::utils;
|
||||
use monolith::opts::Options;
|
||||
use monolith::url;
|
||||
use monolith::utils;
|
||||
|
||||
#[test]
|
||||
fn read_data_url() {
|
||||
@@ -24,25 +25,26 @@ mod passing {
|
||||
options.silent = true;
|
||||
|
||||
// If both source and target are data URLs,
|
||||
// ensure the result contains target data URL
|
||||
let (data, final_url, media_type) = utils::retrieve_asset(
|
||||
// ensure the result contains target data URL
|
||||
let (data, final_url, media_type, charset) = utils::retrieve_asset(
|
||||
cache,
|
||||
&client,
|
||||
"data:text/html;base64,c291cmNl",
|
||||
"data:text/html;base64,dGFyZ2V0",
|
||||
&Url::parse("data:text/html;base64,c291cmNl").unwrap(),
|
||||
&Url::parse("data:text/html;base64,dGFyZ2V0").unwrap(),
|
||||
&options,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&media_type, "text/html");
|
||||
assert_eq!(&charset, "US-ASCII");
|
||||
assert_eq!(
|
||||
url::data_to_data_url(&media_type, &data, &final_url),
|
||||
url::data_to_data_url("text/html", "target".as_bytes(), "")
|
||||
url::create_data_url(&media_type, &charset, &data, &final_url),
|
||||
Url::parse("data:text/html;base64,dGFyZ2V0").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
final_url,
|
||||
url::data_to_data_url("text/html", "target".as_bytes(), "")
|
||||
Url::parse("data:text/html;base64,dGFyZ2V0").unwrap(),
|
||||
);
|
||||
assert_eq!(&media_type, "text/html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -57,31 +59,36 @@ mod passing {
|
||||
|
||||
// Inclusion of local assets from local sources should be allowed
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let (data, final_url, _media_type) = utils::retrieve_asset(
|
||||
let (data, final_url, media_type, charset) = utils::retrieve_asset(
|
||||
cache,
|
||||
&client,
|
||||
&format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-file.html",
|
||||
&Url::parse(&format!(
|
||||
"{file}{cwd}/tests/_data_/basic/local-file.html",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd.to_str().unwrap()
|
||||
),
|
||||
&format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-script.js",
|
||||
))
|
||||
.unwrap(),
|
||||
&Url::parse(&format!(
|
||||
"{file}{cwd}/tests/_data_/basic/local-script.js",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd.to_str().unwrap()
|
||||
),
|
||||
))
|
||||
.unwrap(),
|
||||
&options,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(url::data_to_data_url("application/javascript", &data, &final_url), "data:application/javascript;base64,ZG9jdW1lbnQuYm9keS5zdHlsZS5iYWNrZ3JvdW5kQ29sb3IgPSAiZ3JlZW4iOwpkb2N1bWVudC5ib2R5LnN0eWxlLmNvbG9yID0gInJlZCI7Cg==");
|
||||
assert_eq!(&media_type, "application/javascript");
|
||||
assert_eq!(&charset, "");
|
||||
assert_eq!(url::create_data_url(&media_type, &charset, &data, &final_url), Url::parse("data:application/javascript;base64,ZG9jdW1lbnQuYm9keS5zdHlsZS5iYWNrZ3JvdW5kQ29sb3IgPSAiZ3JlZW4iOwpkb2N1bWVudC5ib2R5LnN0eWxlLmNvbG9yID0gInJlZCI7Cg==").unwrap());
|
||||
assert_eq!(
|
||||
&final_url,
|
||||
&format!(
|
||||
"{file}{cwd}/src/tests/data/basic/local-script.js",
|
||||
final_url,
|
||||
Url::parse(&format!(
|
||||
"{file}{cwd}/tests/_data_/basic/local-script.js",
|
||||
file = file_url_protocol,
|
||||
cwd = cwd.to_str().unwrap()
|
||||
)
|
||||
))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -96,10 +103,11 @@ mod passing {
|
||||
#[cfg(test)]
|
||||
mod failing {
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::opts::Options;
|
||||
use crate::utils;
|
||||
use monolith::opts::Options;
|
||||
use monolith::utils;
|
||||
|
||||
#[test]
|
||||
fn read_local_file_with_data_url_parent() {
|
||||
@@ -113,8 +121,8 @@ mod failing {
|
||||
match utils::retrieve_asset(
|
||||
cache,
|
||||
&client,
|
||||
"data:text/html;base64,SoUrCe",
|
||||
"file:///etc/passwd",
|
||||
&Url::parse("data:text/html;base64,SoUrCe").unwrap(),
|
||||
&Url::parse("file:///etc/passwd").unwrap(),
|
||||
&options,
|
||||
0,
|
||||
) {
|
||||
@@ -139,8 +147,8 @@ mod failing {
|
||||
match utils::retrieve_asset(
|
||||
cache,
|
||||
&client,
|
||||
"https://kernel.org/",
|
||||
"file:///etc/passwd",
|
||||
&Url::parse("https://kernel.org/").unwrap(),
|
||||
&Url::parse("file:///etc/passwd").unwrap(),
|
||||
&options,
|
||||
0,
|
||||
) {
|
||||
Reference in New Issue
Block a user