From 437f5f490b82104c385b087574a81bcfbcd5f4da Mon Sep 17 00:00:00 2001 From: NullSablex <244216261+NullSablex@users.noreply.github.com> Date: Mon, 25 May 2026 18:28:03 -0300 Subject: [PATCH 1/2] =?UTF-8?q?release:=20v1.0.0=20=E2=80=94=20rust-samp?= =?UTF-8?q?=20v3.0.0=20migration=20and=20strict=20mode=20rewrite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major release rebuilding the plugin on top of NullSablex/rust-samp v3.0.0. The same .so/.dll now runs on SA-MP and on Open Multiplayer (native component when dropped into components/, legacy mode when declared under pawn.legacy_plugins in config.json). - Migrate the samp dependency from ZOTTCE/samp-rs to NullSablex/rust-samp v3.0.0; add the required [package.metadata.samp] uid. - Refactor into strict-typed modules: plugin.rs owns an EnvStore, natives live in natives.rs, EnvType is a real enum with TryFrom, DotenvError implements Display + Error, OnceLock global state removed. - Apply clean code passes: helpers for value parsing and buffer writes, inline format args, const fn where applicable, dotenv parser split into named helpers (parse_line, parse_value, strip_quoted, strip_inline_comment, strip_bom). cargo clippy and cargo clippy -- -W pedantic -W nursery clean. - Logger module mirrors mysql_samp: leveled output, banner, logs/env.log with timestamps. Startup no longer prints variable count. - Generate include/env_samp.inc from include/env_samp.inc.in at build time, exposing ENV_SAMP_VERSION pinned to CARGO_PKG_VERSION. - Switch license from GPL-3.0-only to AGPL-3.0-or-later. - Translate runtime messages, examples (examples/example.pwn, examples/env) and the README to en-US. - Replace MANUAL.md with a MkDocs Material site under docs/, published at env-samp.nullsablex.com via GitHub Pages. - Add CI workflows: rust.yml (build/test/clippy/fmt/audit/coverage), release.yml (tag/Cargo.toml gate, cross-build, auto-extracted changelog section, asset upload) and docs.yml (mkdocs --strict + Pages deploy). - Replace scripts/build.sh with scripts/build-linux.sh and scripts/build-windows.sh adapted from mysql_samp. - Move pawn/include/ to include/; rename src/log.rs to src/logger.rs and src/state.rs to src/store.rs. - Add CHANGELOG.md (1.0.0 entry) and archive the previous release under changelog/v0.x.md. - Harden .gitignore with sections for build, secrets, coverage, MkDocs local previews, editor/OS/AI scratch files. --- .github/workflows/docs.yml | 75 ++++ .github/workflows/release.yml | 126 +++++++ .github/workflows/rust.yml | 125 +++++-- .gitignore | 51 ++- CHANGELOG.md | 26 ++ Cargo.lock | 223 ++++------- Cargo.toml | 13 +- LICENSE | 147 ++++---- MANUAL.md | 500 ------------------------- README.md | 185 ++------- build.rs | 48 ++- changelog/v0.x.md | 24 ++ docs/api-reference.md | 45 +++ docs/dotenv-format.md | 52 +++ docs/index.md | 37 ++ docs/installation.md | 47 +++ docs/requirements.txt | 13 + docs/usage.md | 56 +++ .env.example => examples/env | 42 +-- examples/example.pwn | 6 +- {pawn/include => include}/env_samp.inc | 9 +- include/env_samp.inc.in | 43 +++ mkdocs.yml | 93 +++++ scripts/build-linux.sh | 87 +++++ scripts/build-windows.sh | 129 +++++++ scripts/build.sh | 84 ----- src/dotenv.rs | 128 ++++--- src/env_type.rs | 48 +++ src/lib.rs | 128 +------ src/log.rs | 44 --- src/logger.rs | 95 +++++ src/natives.rs | 76 ++++ src/plugin.rs | 37 ++ src/state.rs | 20 - src/store.rs | 24 ++ 35 files changed, 1624 insertions(+), 1262 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md delete mode 100644 MANUAL.md create mode 100644 changelog/v0.x.md create mode 100644 docs/api-reference.md create mode 100644 docs/dotenv-format.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/requirements.txt create mode 100644 docs/usage.md rename .env.example => examples/env (52%) rename {pawn/include => include}/env_samp.inc (80%) create mode 100644 include/env_samp.inc.in create mode 100644 mkdocs.yml create mode 100755 scripts/build-linux.sh create mode 100755 scripts/build-windows.sh delete mode 100755 scripts/build.sh create mode 100644 src/env_type.rs delete mode 100644 src/log.rs create mode 100644 src/logger.rs create mode 100644 src/natives.rs create mode 100644 src/plugin.rs delete mode 100644 src/state.rs create mode 100644 src/store.rs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..da5c758 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,75 @@ +name: Docs + +on: + push: + branches: + - master + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'docs/requirements.txt' + - '.github/workflows/docs.yml' + pull_request: + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'docs/requirements.txt' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + +# Only one Pages deployment can run at a time. Cancelling a queued +# deployment in favour of a newer one is fine; cancelling one already +# in flight would orphan a partial deploy, so we let it finish. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build site + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: '3.x' + cache: pip + cache-dependency-path: docs/requirements.txt + + - name: Install MkDocs and theme + run: pip install -r docs/requirements.txt + + - name: Strict build (fails on warnings) + run: mkdocs build --strict + + # Upload the generated site so the deploy job (or anyone debugging + # a PR) can grab it. Uses the dedicated Pages artifact format. + - name: Upload Pages artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + name: Deploy to GitHub Pages + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + # `actions/deploy-pages` needs `pages: write` to publish and + # `id-token: write` for OIDC verification of the artifact. + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5db294d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,126 @@ +name: Release + +on: + release: + types: [published, prereleased] + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + PLUGIN_NAME: env_samp + +jobs: + build-release: + runs-on: ubuntu-latest + # Needs write access to attach .so/.dll/.inc to the GitHub release + # and to amend the release body with the auto-generated notes. + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + + - name: Verify Cargo.toml version matches release tag + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + CARGO_VERSION="$(grep -m1 '^version' Cargo.toml | sed 's/.*= *"\(.*\)"/\1/')" + echo "Release tag: ${GITHUB_REF_NAME} (version=${TAG_VERSION})" + echo "Cargo.toml: ${CARGO_VERSION}" + if [ "${TAG_VERSION}" != "${CARGO_VERSION}" ]; then + echo "::error::Release tag (${GITHUB_REF_NAME}) does not match Cargo.toml version (${CARGO_VERSION})." + echo "::error::Bump Cargo.toml to ${TAG_VERSION} (or retag) and try again." + exit 1 + fi + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: i686-unknown-linux-gnu,i686-pc-windows-msvc + + - name: Install cross-compilation tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-multilib g++-multilib clang llvm + + - name: Install cargo-xwin + run: cargo install cargo-xwin --locked + + - name: Cache cargo + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + ~/.cache/cargo-xwin + target + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + + - name: Build Linux (i686, SA-MP + native OMP) + run: cargo build --release --target i686-unknown-linux-gnu + + - name: Build Windows (i686 MSVC, SA-MP + native OMP) + run: cargo xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc + + - name: Stage release artifacts + run: | + mkdir -p dist + cp "target/i686-unknown-linux-gnu/release/lib${PLUGIN_NAME}.so" "dist/${PLUGIN_NAME}.so" + cp "target/i686-pc-windows-msvc/release/${PLUGIN_NAME}.dll" "dist/${PLUGIN_NAME}.dll" + cp "include/${PLUGIN_NAME}.inc" "dist/${PLUGIN_NAME}.inc" + + - name: Generate release notes + run: | + SDK_VERSION="$(grep -oP 'tag\s*=\s*"\Kv[0-9][^"]*' Cargo.toml | head -n1)" + PLUGIN_VERSION="$(grep -m1 '^version' Cargo.toml | sed 's/.*= *"\(.*\)"/\1/')" + + # Extract the section for the current version from CHANGELOG.md. + # Format: "## [X.Y.Z] — yyyy/mm/dd" up to the next "## " heading. + CHANGELOG_SECTION="" + if [ -f CHANGELOG.md ]; then + CHANGELOG_SECTION="$(awk -v ver="${PLUGIN_VERSION}" ' + $0 ~ "^## \\[" ver "\\]" { capture = 1; next } + capture && /^## / { exit } + capture { print } + ' CHANGELOG.md)" + fi + + cat > release_body.md <> release_body.md + fi + + cat release_body.md + + - name: Upload release assets + uses: softprops/action-gh-release@v3 + with: + body_path: release_body.md + append_body: true + files: | + dist/${{ env.PLUGIN_NAME }}.so + dist/${{ env.PLUGIN_NAME }}.dll + dist/${{ env.PLUGIN_NAME }}.inc diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 92222a8..153ada0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,41 +9,100 @@ on: env: CARGO_TERM_COLOR: always +# Default: read-only. Each job opts in to extra scopes when needed. +permissions: + contents: read + jobs: build: + name: Build, test, clippy runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Install Rust (stable + clippy) + uses: dtolnay/rust-toolchain@stable + with: + targets: i686-unknown-linux-gnu + components: clippy + + - name: Install cross-compilation tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-multilib g++-multilib + + - uses: Swatinem/rust-cache@v2 + + - name: Build + run: cargo build --target i686-unknown-linux-gnu --verbose + - name: Test + run: cargo test --target i686-unknown-linux-gnu --verbose + + - name: Clippy + run: cargo clippy --target i686-unknown-linux-gnu --all-targets -- -D warnings + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: i686-unknown-linux-gnu - - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v3 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v3 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - - name: Build - run: cargo build --verbose - - - name: Run tests - run: cargo test --verbose - - - name: Run clippy - run: cargo clippy --all-targets -- -D warnings + - uses: actions/checkout@v6 + - name: Install Rust (stable + rustfmt) + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + audit: + name: Security audit + runs-on: ubuntu-latest + # rustsec/audit-check opens issues for new advisories and posts + # review comments on PRs that introduce vulnerable dependencies. + permissions: + contents: read + issues: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v6 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + coverage: + name: Coverage (llvm-cov) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Install Rust (stable + llvm-tools) + uses: dtolnay/rust-toolchain@stable + with: + targets: i686-unknown-linux-gnu + components: llvm-tools-preview + + - name: Install cross-compilation tools + run: | + sudo apt-get update + sudo apt-get install -y gcc-multilib g++-multilib + + - uses: Swatinem/rust-cache@v2 + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Generate LCOV report + run: cargo llvm-cov --target i686-unknown-linux-gnu --lcov --output-path lcov.info + + - name: Upload coverage artifact + uses: actions/upload-artifact@v7 + with: + name: coverage-lcov + path: lcov.info + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 5a811cd..5a6bea4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,52 @@ +# Rust build artifacts /target +**/*.rs.bk +*.pdb + +# Plugin distribution output (regenerated by scripts/build-*.sh) /dist + +# MkDocs site output (regenerated by `mkdocs build`) +/site + +# Runtime logs the plugin writes (logs/env.log) +/logs + +# Secrets — never commit a real .env. Use examples/env as the template. .env -!.env.example -.claude \ No newline at end of file +.env.* + +# Code coverage output (cargo-llvm-cov, grcov) +lcov.info +*.profraw +*.profdata +/coverage + +# Python virtualenvs used to preview docs locally (`mkdocs serve`) +.venv/ +venv/ +env/ +__pycache__/ +*.pyc + +# Editor / IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS junk +.DS_Store +Thumbs.db +desktop.ini + +# AI assistant scratch +.claude +.cursor/ +.aider* + +# Local override files +*.local +*.bak +*.orig diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..48174bb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +All notable changes to this project are documented in this file. + +Format inspired by [Keep a Changelog](https://keepachangelog.com/). Versioning follows [Semantic Versioning](https://semver.org/). Older entries live under [`changelog/`](changelog/). + +## [1.0.0] — 2026/05/25 + +Initial public release. Built on top of [rust-samp v3.0.0](https://github.com/NullSablex/rust-samp/releases/tag/v3.0.0). The same `.so` / `.dll` loads on SA-MP and on Open Multiplayer (native component or legacy mode). + +### Added + +- **Universal binary.** A single artifact runs on SA-MP and on Open Multiplayer. Open Multiplayer auto-loads it as a native component when dropped into the `components/` folder (no `config.json` entry needed — the folder itself is the registration), or in legacy mode when dropped into `plugins/` and declared under `pawn.legacy_plugins` in `config.json`. +- Natives `Env(key, dest, type, dest_len)` and `EnvCount()` covering `ENV_STRING`, `ENV_INT`, `ENV_FLOAT` and `ENV_BOOL`. Unknown type values fall back to `ENV_STRING` with a warning. +- `.env` parser with single/double quoting, escape sequences (`\n`, `\r`, `\t`, `\\`, `\"`), inline `#` comments (require preceding whitespace), `export ` skip, BOM stripping and duplicate-key "last wins" semantics. +- `ENV_SAMP_VERSION` constant in `env_samp.inc` — string with the plugin version, auto-generated from `CARGO_PKG_VERSION` by `build.rs`. +- Logger with banner and detailed log file at `logs/env.log`. Configurable level (0–4); values are never logged. +- Hard limit of 1 MiB on the `.env` file size — rejected with a warning if exceeded. +- MkDocs Material documentation site published at . +- CI: `build`/`test`/`clippy`/`fmt`/`audit`/`coverage` jobs (`.github/workflows/rust.yml`), release workflow with tag-vs-`Cargo.toml` sanity check and auto-extracted changelog section (`.github/workflows/release.yml`), MkDocs strict build + GitHub Pages deploy (`.github/workflows/docs.yml`). + +### Security + +- The `.env` is read **once** at startup. No runtime reload. +- Values are **never** logged. Only error/warning messages about the file itself are emitted. +- Distributed under the [GNU Affero General Public License v3.0 or later](LICENSE) — modifications used on servers accessible over the network must have their source offered to the users interacting with that instance. diff --git a/Cargo.lock b/Cargo.lock index dcb67c3..b65f176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,27 +13,27 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -47,9 +47,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -58,17 +58,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "colored" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" -dependencies = [ - "is-terminal", - "lazy_static", - "winapi", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -77,19 +66,18 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "env_samp" -version = "0.1.0" +version = "1.0.0" dependencies = [ - "log", + "chrono", "samp", ] [[package]] name = "fern" -version = "0.5.9" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69ab0d5aca163e388c3a49d284fed6c3d0810700e77c5ae2756a50ec1a4daaa" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" dependencies = [ - "chrono", "log", ] @@ -100,10 +88,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "hermit-abi" -version = "0.5.2" +name = "futures-core" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] [[package]] name = "iana-time-zone" @@ -129,38 +135,23 @@ dependencies = [ "cc", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" @@ -179,18 +170,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "proc-macro2" -version = "0.4.30" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "proc-macro2" @@ -201,22 +189,13 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ - "proc-macro2 1.0.106", + "proc-macro2", ] [[package]] @@ -227,31 +206,31 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "samp" -version = "0.1.3" -source = "git+https://github.com/ZOTTCE/samp-rs?rev=17ecb2f722bf0d74a98eb782745cd77e41c3c60a#17ecb2f722bf0d74a98eb782745cd77e41c3c60a" +version = "3.0.0" +source = "git+https://github.com/NullSablex/rust-samp.git?tag=v3.0.0#a08348d1c958e29b9a730d93eee3cae7d1fdc4e3" dependencies = [ "fern", + "log", "samp-codegen", "samp-sdk", ] [[package]] name = "samp-codegen" -version = "0.1.2" -source = "git+https://github.com/ZOTTCE/samp-rs?rev=17ecb2f722bf0d74a98eb782745cd77e41c3c60a#17ecb2f722bf0d74a98eb782745cd77e41c3c60a" +version = "1.3.0" +source = "git+https://github.com/NullSablex/rust-samp.git?tag=v3.0.0#a08348d1c958e29b9a730d93eee3cae7d1fdc4e3" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "syn 0.15.44", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "samp-sdk" -version = "0.9.2" -source = "git+https://github.com/ZOTTCE/samp-rs?rev=17ecb2f722bf0d74a98eb782745cd77e41c3c60a#17ecb2f722bf0d74a98eb782745cd77e41c3c60a" +version = "3.0.0" +source = "git+https://github.com/NullSablex/rust-samp.git?tag=v3.0.0#a08348d1c958e29b9a730d93eee3cae7d1fdc4e3" dependencies = [ "bitflags", - "colored", ] [[package]] @@ -261,15 +240,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "syn" -version = "0.15.44" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "syn" @@ -277,8 +251,8 @@ version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ - "proc-macro2 1.0.106", - "quote 1.0.44", + "proc-macro2", + "quote", "unicode-ident", ] @@ -288,17 +262,11 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -309,58 +277,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ - "quote 1.0.44", + "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", - "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "proc-macro2", + "quote", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.62.2" @@ -380,9 +326,9 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -391,9 +337,9 @@ version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -419,12 +365,3 @@ checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] diff --git a/Cargo.toml b/Cargo.toml index 3333c70..345d3c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,18 @@ [package] name = "env_samp" -version = "0.1.0" +version = "1.0.0" edition = "2024" authors = ["NullSablex"] repository = "https://github.com/NullSablex/env_samp" homepage = "https://github.com/NullSablex/env_samp" -license = "GPL-3.0-only" +license = "AGPL-3.0-or-later" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib", "lib"] [dependencies] -samp = { git = "https://github.com/ZOTTCE/samp-rs", rev = "17ecb2f722bf0d74a98eb782745cd77e41c3c60a" } -log = "0.4" +samp = { git = "https://github.com/NullSablex/rust-samp.git", tag = "v3.0.0" } +chrono = "0.4" + +[package.metadata.samp] +uid = "0xda6b28e7df4a9f5f" \ No newline at end of file diff --git a/LICENSE b/LICENSE index f288702..be3f7b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by + it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/MANUAL.md b/MANUAL.md deleted file mode 100644 index 873c2ed..0000000 --- a/MANUAL.md +++ /dev/null @@ -1,500 +0,0 @@ -# Manual de Uso - env_samp - -Documentação completa da API Pawn para o plugin de variaveis de ambiente env_samp. - -## Instalacao Basica - -### 1. Include no seu gamemode - -```pawn -#include -``` - -O arquivo `env_samp.inc` ja contem as definicoes das natives e constantes de tipo. - -### 2. Natives disponiveis - -```pawn -native bool:Env(const key[], dest[], type = ENV_STRING, dest_len = sizeof(dest)); -native EnvCount(); -``` - ---- - -## API Reference - -### Env() - -Busca uma variavel de ambiente do arquivo `.env` e a escreve no buffer `dest` com o tipo especificado. - -**Assinatura:** -```pawn -bool:Env(const key[], dest[], type = ENV_STRING, dest_len = sizeof(dest)) -``` - -**Parametros:** - -| Parametro | Tipo | Descricao | -|-----------|------|-----------| -| `key` | `const char[]` | Nome da variavel a buscar (case-sensitive) | -| `dest` | Buffer | Local onde o valor sera armazenado | -| `type` | `i32` | Tipo de dado esperado (veja tabela abaixo) | -| `dest_len` | `i32` | Tamanho do buffer (auto-calculado por `sizeof(dest)` por padrao) | - -**Tipos suportados:** - -| Constante | Valor | Comportamento | -|-----------|-------|---------------| -| `ENV_STRING` | `0` | Copia a string literal para `dest` | -| `ENV_INT` | `1` | Converte para inteiro e escreve em `dest[0]` | -| `ENV_FLOAT` | `2` | Converte para float e escreve em `dest[0]` | -| `ENV_BOOL` | `3` | Converte para booleano (`0` ou `1`) em `dest[0]` | - -**Retorno:** - -- `true` — Variavel encontrada e valor copiado/convertido com sucesso -- `false` — Variavel nao encontrada (buffer recebe valor padrao/zero) - -**Exemplo basico:** - -```pawn -new host[64]; -if (Env("MYSQL_HOST", host)) -{ - printf("Conectando em %s", host); -} -else -{ - printf("Variavel MYSQL_HOST nao definida"); -} -``` - ---- - -### EnvCount() - -Retorna o total de variaveis carregadas do arquivo `.env` no startup. - -**Assinatura:** -```pawn -EnvCount() -``` - -**Retorno:** Inteiro com o total de chaves carregadas. - -**Exemplo:** -```pawn -printf("Total de variaveis: %d", EnvCount()); -``` - ---- - -## Exemplos de Uso - -### Exemplo 1: Dimensionamento correto de buffers - -Em Pawn, o tamanho do buffer (ex: `[64]`) define quantos **caracteres** podem ser armazenados: - -```pawn -// Nomes de host sao pequenos -new host[64]; // 63 caracteres + 1 null terminator -Env("MYSQL_HOST", host); - -// Senhas podem ser longas -new pass[128]; // 127 caracteres + 1 null terminator -Env("MYSQL_PASS", pass); - -// URLs podem ser muito longas -new api_url[512]; // 511 caracteres + 1 null terminator -Env("API_URL", api_url); -``` - -**Regra simples:** o tamanho do array = numero de caracteres esperado + 1 (para null terminator). - -**Exemplo completo:** - -```pawn -new host[64], port, user[32], pass[128], db[32]; - -// Carregar todas as variaveis -Env("MYSQL_HOST", host); -Env("MYSQL_PORT", port, ENV_INT); -Env("MYSQL_USER", user); -Env("MYSQL_PASS", pass); -Env("MYSQL_DB", db); - -printf("[DB] Conectando em %s:%d usuario=%s banco=%s", host, port, user, db); -``` - -Se a variavel no `.env` for maior que o buffer, ela e **truncada silenciosamente**. - -### Exemplo 2: Configuracoes booleanas e float - -```pawn -new bool:debug, Float:tick_rate; - -Env("APP_DEBUG", debug, ENV_BOOL); -Env("TICK_RATE", tick_rate, ENV_FLOAT); - -if (debug) -{ - printf("[CONFIG] Debug ativado, tick rate=%.2f", tick_rate); -} -``` - -### Exemplo 3: Validacao com valores padrao - -```pawn -new port; -if (!Env("SERVER_PORT", port, ENV_INT)) -{ - port = 7777; // Valor padrao - printf("[CONFIG] PORT nao definido, usando 7777"); -} -``` - -### Exemplo 4: Loop por todas as variaveis - -```pawn -printf("Total de variaveis carregadas: %d", EnvCount()); -// Nota: O plugin nao oferece API para iterar por chave/valor. -// Use EnvCount() apenas para debugging ou diagnosticos. -``` - ---- - -## Arquivo `.env` - Formato - -> [!NOTE] -> O arquivo `.env` e lido apenas uma vez no startup do servidor. Alteracoes requerem reinicializacao. - -### Formato valido - -```env -# Comentarios iniciados com # -CHAVE=valor - -# Espacos sao trimmed -DB_HOST = 127.0.0.1 -DB_PORT = 3306 - -# Aspas simples (valores literais, sem escapes) -LITERAL='texto com \n literal' - -# Aspas duplas (interpreta escapes) -MOTD="Bem-vindo!\nServidor ativo" - -# Comentario inline (requer espaco antes de #) -API_KEY=abc123def456 # sua chave aqui - -# Hash sem espaco faz parte do valor -HASH_VALUE=abc#def - -# Chaves duplicadas: ultima vence -DEBUG=false -DEBUG=true -``` - -### Escapes suportados em aspas duplas - -| Escape | Resultado | -|--------|-----------| -| `\\` | Barra invertida `\` | -| `\"` | Aspas duplas `"` | -| `\n` | Quebra de linha | -| `\r` | Retorno de carro | -| `\t` | Tabulacao | - -### Formatos nao suportados - -```env -# Nao aceito: prefixo export -export DB_HOST=127.0.0.1 - -# Nao aceito: valores multiline -MULTILINE="linha1 -linha2" - -# Nao aceito: expansao de variaveis -EXPANDED=${DB_HOST}:3306 -``` - ---- - -## Dimensionamento de Buffers - -Em Pawn, o tamanho do array determina quantos caracteres **no maximo** podem ser armazenados. - -### Formula - -``` -Tamanho do buffer = Numero maximo de caracteres esperados + 1 (null terminator) -``` - -**Resumo prático:** -> Sempre some 1 ao tamanho esperado. Se quer armazenar 50 caracteres, use `[51]`. Se quer algo genérico, arredonde para cima e adicione 1. - -### Tabela de referencia - -Use esta tabela como guia para dimensionar seus buffers: - -| Tipo de dado | Tamanho sugerido | Motivo | -|--------------|-----------------|--------| -| Hostname/IP | `[64]` ou `[128]` | IPv4 simples (15 chars), mas hostname pode ser longo | -| Porta (string) | `[8]` ou `[16]` | Porta em string (max 5 numeros) | -| Usuario banco | `[32]` ou `[64]` | Nomes de usuario sao geralmente curtos | -| Senha | `[128]` ou `[256]` | Senhas devem ser longas para seguranca | -| Nome banco | `[64]` | Nomes de banco sao razoavelmente curtos | -| URL/URI | `[512]` ou `[1024]` | URLs podem ser muito longas | -| Token/API Key | `[256]` | Tokens sao geralmente longos | -| Mensagem/String generica | `[256]` a `[512]` | Depende do conteudo | - -### Exemplos praticos - -```pawn -// Curto -new lang[8]; // "pt_BR" = 5 chars -Env("LANGUAGE", lang); - -// Medio -new email[128]; // emails podem ter ~100 chars -Env("ADMIN_EMAIL", email); - -// Longo -new api_key[256]; // chaves sao longas -Env("API_SECRET_KEY", api_key); - -// Muito longo -new conn_str[512]; // connection strings podem ser complexas -Env("DATABASE_URL", conn_str); -``` - -### O que acontece se for pequeno demais? - -Se o valor no `.env` for **maior** que o buffer: - -```pawn -// .env -MYSQL_PASS=SenhaRealmenteLongaDemasiado123!@# - -// Codigo - ERRADO, buffer pequeno -new pass[20]; -Env("MYSQL_PASS", pass); -// pass contera apenas: "SenhaRealmenteLongaD" (truncado) -``` - -**Solucao:** aumente o tamanho do buffer - -```pawn -// Codigo - CORRETO, buffer adequado -new pass[256]; // Suficiente para qualquer senha -Env("MYSQL_PASS", pass); -``` - -### Dica de seguranca - -Para senhas e tokens, **sempre use buffers generosos**: - -```pawn -// Senhas -new db_pass[256]; // Nao poupe tamanho - -// API Keys -new api_token[512]; // Chaves modernas sao longas - -// Connection Strings -new conn_str[1024]; // Conexoes podem ter query strings complexas -``` - ---- - -## Comportamento e Garantias - -> [!IMPORTANT] -> Leia isto para evitar erros em producao. - -### Leitura unica no startup - -- O arquivo `.env` e lido exatamente uma vez quando o servidor inicia. -- **Nao ha releitura em runtime.** -- Se modificar `.env`, **reinicie o servidor.** - -### Valores nunca sao logados - -- Os valores das variaveis **nunca aparecem em logs ou console** (seguranca). -- Apenas o total de variaveis carregadas e logado. -- Evita exposicao de senhas ou tokens em arquivos de log. - -### Tamanho maximo - -- Arquivos `.env` com mais de **1 MB sao rejeitados.** -- Se o servidor falhar a carregar, verifique o tamanho. - -### Protecao contra corrompcao UTF-8 - -- Arquivos salvos com BOM UTF-8 (comum no Notepad do Windows) sao tratados corretamente. -- Truncacao de valores e feita em fronteira de caractere, nunca no meio de um caractere multi-byte. - -### Conflitos de chaves - -- Se a mesma chave aparece multiplas vezes no `.env`, a **ultima ocorrencia vence.** - ---- - -## Diagnosticos - -### Variavel nao encontrada? - -1. **Confira o nome exacto** — e case-sensitive (`MYSQL_HOST` != `mysql_host`) -2. **Confira o arquivo `.env`** — deve estar na raiz do servidor (mesmo diretorio do executavel) -3. **Reinicie o servidor** — mudar `.env` so funciona apos reinicializacao -4. **Verifique o tipo** — se definiu `Env("PORT", var, ENV_INT)`, a variavel deve ser um inteiro valido - -### Conversao falha? - -- `ENV_INT` — se o valor nao for um inteiro valido, retorna `false` e `dest[0] = 0` -- `ENV_FLOAT` — se o valor nao for um float valido, retorna `false` e `dest[0] = 0.0` -- `ENV_BOOL` — aceita `true`, `1`, `yes`, `on` (true) ou `false`, `0`, `no`, `off` (false) - -### String truncada? - -- Se a string no `.env` for maior que o buffer Pawn, ela e truncada automaticamente. -- Use buffers maiores se necessario: `new var[256];` ao inves de `new var[32];` - ---- - -## Boas Praticas - -> [!TIP] -> Siga estas orientacoes para codigo robusto em producao. - -### 1. Sempre validar retorno - -```pawn -// Ruim -Env("DB_HOST", host); - -// Bom -if (!Env("DB_HOST", host)) -{ - host = "127.0.0.1"; // Padrao -} -``` - -### 2. Usar template `.env.example` - -Mantenha um arquivo `.env.example` com **todas as chaves esperadas** mas sem valores reais: - -```env -# .env.example (commit no git) -MYSQL_HOST= -MYSQL_PORT= -MYSQL_USER= -MYSQL_PASS= -APP_DEBUG=false -``` - -Usuarios fazem uma copia: -```bash -cp .env.example .env -# ... editar .env com valores reais ... -``` - -### 3. Nao versionem `.env` - -Adicione ao `.gitignore`: -``` -.env -``` - -### 4. Carregar variaveis cedo - -Carregue todas as variaveis no `OnGameModeInit()` ou `OnFilterScriptInit()`, nao no meio da execucao: - -```pawn -public OnGameModeInit() -{ - // Fazer isto aqui - new host[64]; - Env("MYSQL_HOST", host); - - // Nao aqui (custoso repetir) - return 1; -} - -public SomeCallback() -{ - // Evitar - // new host[64]; - // Env("MYSQL_HOST", host); // Ja foi carregado acima -} -``` - -### 5. Documentar variaveis esperadas - -No seu `.env.example`, adicione comentarios: - -```env -# Banco de dados -MYSQL_HOST= -MYSQL_PORT=3306 -MYSQL_USER= -MYSQL_PASS= - -# Debug -APP_DEBUG=false -LOG_LEVEL=INFO -``` - ---- - -## Troubleshooting - -| Problema | Causa | Solucao | -|----------|-------|---------| -| `Env()` retorna `false` para chaves validas | Chave nao definida no `.env` ou `.env` nao existe | Verif ficheiro `.env` e reinicie | -| Variavel com caracteres estranhos | BOM UTF-8 no arquivo | Salve como UTF-8 sem BOM | -| String truncada no meio de acentos | Buffer pequeno demais | Aumente tamanho do buffer | -| Conversao INT/FLOAT falha | Valor nao e numero valido | Valide o `.env` | -| `EnvCount()` retorna 0 | Arquivo `.env` nao existe ou esta vazio | Crie `.env` com variaveis | - ---- - -## Seguranca - -> [!WARNING] -> Proteja seus arquivos `.env` como dados sensiveis. - -### Do's - -- ✅ Mantenha `.env` fora do versionamento (use `.gitignore`) -- ✅ Use permissoes de arquivo restritivas: `chmod 600 .env` -- ✅ Nunca commite `.env` real no repositorio -- ✅ Use `.env.example` como template publico -- ✅ Rotacione senhas regularmente - -### Don'ts - -- ❌ Nao commet o arquivo `.env` real -- ❌ Nao compartilhe credenciais em Slack/Discord -- ❌ Nao versione chaves de API ou senhas -- ❌ Nao deixe o servidor aberto com `.env` visivelmente sensivel - ---- - -## Limitacoes Conhecidas - -- **Nao ha iteracao**: nao e possivel listar todas as chaves, apenas buscar por nome -- **Nao ha releitura**: `.env` e lido uma unica vez no startup -- **Nao ha expansao**: `${VAR}` nao e suportado -- **Nao ha multiline**: quebras de linha dentro de valores nao sao suportadas (use `\n` com aspas duplas) -- **Tamanho maximo de 1 MB**: arquivos maiores sao rejeitados - ---- - -## Versao - -Plugin: **v0.1.0** -Ultima atualizacao: 2026-02-17 - -Para relatorios de bugs ou sugestoes: [GitHub Issues](https://github.com/NullSablex/env_samp/issues) diff --git a/README.md b/README.md index fe524f6..eb72714 100644 --- a/README.md +++ b/README.md @@ -3,89 +3,31 @@ [![Language](https://img.shields.io/badge/Rust-2024%20edition-orange)]() [![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20Windows-blue)]() [![Architecture](https://img.shields.io/badge/arch-i686%20(32--bit)-lightgrey)]() +[![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-green)]() -Plugin SA:MP escrito em Rust que carrega variaveis de ambiente a partir de um arquivo `.env` durante o startup do servidor e as expoe para scripts Pawn por meio de uma unica native tipada. +Environment variables (`.env`) plugin for SA-MP and Open Multiplayer, written in Rust. Loads a `.env` file at startup and exposes the values to Pawn through a single typed native. -Projetado para eliminar credenciais hardcoded no codigo Pawn, mantendo dados sensiveis fora do versionamento e centralizados em um unico arquivo de configuracao. +Designed to eliminate hardcoded credentials in Pawn code, keeping secrets out of version control and centralized in a single configuration file. -## Autor +The same `.so` / `.dll` runs on SA-MP and on Open Multiplayer — natively as a component (recommended) or via legacy mode. -**NullSablex** - Desenvolvimento e manutencao +## Documentation -## Caracteristicas +Full documentation lives at ****: -- Leitura unica do `.env` no startup (sem overhead em runtime) -- Native unica `Env()` com suporte a tipos: string, int, float e bool -- Parser robusto com suporte a aspas simples, duplas, escapes e comentarios -- Formato flexivel: `CHAVE=VALOR` e `CHAVE = VALOR` sao equivalentes -- Estado global thread-safe via `OnceLock` -- Protecao contra BOM UTF-8 (comum em arquivos criados no Windows) -- Truncacao segura em fronteira de caractere (previne corrupcao UTF-8) -- Limite de 1 MB para prevenir uso indevido do arquivo `.env` -- Valores nunca expostos em logs +- [Installation](https://env-samp.nullsablex.com/installation/) +- [Usage](https://env-samp.nullsablex.com/usage/) +- [.env format](https://env-samp.nullsablex.com/dotenv-format/) +- [API reference](https://env-samp.nullsablex.com/api-reference/) -## Natives +The Markdown sources are in [docs/](docs/) — `mkdocs serve` from the repo root for a local preview. -### Env +## Examples -```pawn -native bool:Env(const key[], dest[], type = ENV_STRING, dest_len = sizeof(dest)); -``` - -Busca o valor associado a `key` no `.env` e escreve em `dest` no tipo solicitado. - -**Tipos disponiveis:** - -| Constante | Valor | Comportamento | -|-------------|-------|---------------| -| `ENV_STRING` | `0` | Copia a string para `dest` (padrao) | -| `ENV_INT` | `1` | Converte para inteiro e escreve em `dest` | -| `ENV_FLOAT` | `2` | Converte para float e escreve em `dest` | -| `ENV_BOOL` | `3` | Converte para bool (`0`/`1`) e escreve em `dest` | - -**Retorno:** `true` se a chave foi encontrada e o valor convertido com sucesso, `false` caso contrario. - -**Valores bool reconhecidos:** `true`, `1`, `yes`, `on` / `false`, `0`, `no`, `off`. - -### EnvCount - -```pawn -native EnvCount(); -``` - -Retorna o total de variaveis carregadas do `.env`. - -## Instalacao - -1. Copie o plugin para a pasta `plugins/` do servidor: - - **Linux:** `env_samp.so` - - **Windows:** `env_samp.dll` - -2. Adicione ao `server.cfg`: - - ``` - # Linux - plugins env_samp.so +- [examples/example.pwn](examples/example.pwn) — minimal gamemode using `Env` for string/int/float/bool and `EnvCount`. +- [examples/env](examples/env) — annotated `.env` covering every syntax feature the parser accepts (quoting, escapes, inline comments, duplicates, etc.). Copy it to `.env` at the server root and edit the values. - # Windows - plugins env_samp.dll - ``` - -3. Copie `pawn/include/env_samp.inc` para a pasta de includes do compilador Pawn. - -4. Crie o arquivo `.env` na raiz do servidor (mesmo diretorio do executavel): - - ```env - MYSQL_HOST = 127.0.0.1 - MYSQL_PORT = 3306 - MYSQL_USER = samp_user - MYSQL_PASS = senha_segura - MYSQL_DB = meu_banco - APP_DEBUG = false - TICK_RATE = 0.5 - ``` - -## Exemplo de uso +## Quick start ```pawn #include @@ -93,99 +35,50 @@ Retorna o total de variaveis carregadas do `.env`. public OnGameModeInit() { - printf("Variaveis carregadas: %d", EnvCount()); - - // String (tipo padrao, basta omitir o terceiro argumento) new host[64]; - Env("MYSQL_HOST", host); + if (Env("MYSQL_HOST", host)) + { + printf("[env_samp] MYSQL_HOST=%s", host); + } - // Integer new port; - Env("MYSQL_PORT", port, ENV_INT); - - // Float - new Float:rate; - Env("TICK_RATE", rate, ENV_FLOAT); + if (Env("MYSQL_PORT", port, ENV_INT)) + { + printf("[env_samp] MYSQL_PORT=%d", port); + } - // Bool - new bool:debug; - Env("APP_DEBUG", debug, ENV_BOOL); - - printf("[DB] %s:%d db_debug=%d tick=%f", host, port, _:debug, rate); return 1; } ``` -## Formato do .env - -| Formato | Exemplo | Resultado | -|---|---|---| -| Basico | `KEY=value` | `value` | -| Com espacos | `KEY = value` | `value` | -| Aspas simples | `KEY='val\nue'` | `val\nue` (literal) | -| Aspas duplas | `KEY="val\nue"` | `val` + quebra de linha + `ue` | -| Comentario inline | `KEY=value # comment` | `value` | -| Hash colado | `KEY=val#ue` | `val#ue` | -| Valor vazio | `KEY=` | (string vazia) | -| Chave duplicada | ultima ocorrencia vence | | - -**Ignorados:** linhas vazias, linhas iniciadas com `#`, linhas com prefixo `export`. - -**Escapes em aspas duplas:** `\\`, `\"`, `\n`, `\r`, `\t`. +Run the [example .env](examples/env) alongside your server and call this from `OnGameModeInit`. ## Build -Requer Rust (stable) com os targets de 32-bit instalados: +Requires Rust stable with the 32-bit targets installed. On Linux: ```bash -# Build completo (Linux + Windows) -./scripts/build.sh - -# Build manual para um unico target -cargo build --release --target i686-unknown-linux-gnu -cargo build --release --target i686-pc-windows-gnu +./scripts/build-linux.sh # builds .so + .dll into dist/ ``` -Artefatos gerados em `dist/`: +On Windows (Git Bash): +```bash +./scripts/build-windows.sh ``` -dist/ - env_samp.so # Linux plugin - env_samp.so.sha256 - env_samp.dll # Windows plugin - env_samp.dll.sha256 -``` - -## Seguranca -- O `.env` e lido **uma unica vez** no startup. Nao ha releitura em runtime. -- **Valores nunca sao expostos em logs.** Apenas a quantidade de variaveis carregadas e registrada. -- Arquivos acima de 1 MB sao rejeitados automaticamente. -- Adicione `.env` ao `.gitignore`. **Nunca versione credenciais.** -- Use `.env.example` como template publico sem valores reais. +Both scripts read `PLUGIN_NAME` from `Cargo.toml`, install missing rustup targets and produce `dist/env_samp.so` and `dist/env_samp.dll`. -## Estrutura do projeto +## Security -``` -env_samp/ - src/ - lib.rs Entry point, native e lifecycle do plugin - dotenv.rs Parser .env com suite de testes - state.rs Estado global thread-safe (OnceLock) - log.rs Logging e banner de inicializacao - pawn/ - include/ - env_samp.inc Include com native e constantes de tipo - examples/ - example.pwn Exemplo de uso em gamemode - scripts/ - build.sh Cross-compilation Linux + Windows 32-bit - build.rs Build script para timestamp de compilacao - .env.example Template de variaveis -``` +- The `.env` is read **once** at startup. No runtime reload. +- Values are **never** logged. Only error/warning messages about the file itself are emitted. +- Files larger than 1 MiB are rejected. +- Add `.env` to `.gitignore`. **Never commit credentials.** +- Use [examples/env](examples/env) as a public template without real values. -## Licenca +## License -Este projeto e distribuido sob a [GNU General Public License v3.0](LICENSE). +This project is distributed under the [GNU Affero General Public License v3.0 or later](LICENSE). -Voce pode usar, modificar e redistribuir livremente, desde que qualquer trabalho derivado tambem seja distribuido sob a mesma licenca com o codigo-fonte disponivel. +You can use, modify and redistribute it freely, provided that derivative works are released under the same license with source code available. Because it is AGPL, modifications used on servers accessible over the network must also have their source offered to the users interacting with that instance. diff --git a/build.rs b/build.rs index cdb9cd2..f2ffeb1 100644 --- a/build.rs +++ b/build.rs @@ -6,24 +6,44 @@ fn main() { .filter(|o| o.status.success()) .and_then(|o| String::from_utf8(o.stdout).ok()); - let (date, time, year) = match output { - Some(s) => { - let s = s.trim().to_string(); - let parts: Vec<&str> = s.splitn(3, '|').collect(); + let (date, time, year) = output.map_or_else( + || (unknown(), unknown(), unknown()), + |raw| { + let trimmed = raw.trim(); + let parts: Vec<&str> = trimmed.splitn(3, '|').collect(); ( - parts.first().unwrap_or(&"Unknown").to_string(), - parts.get(1).unwrap_or(&"Unknown").to_string(), - parts.get(2).unwrap_or(&"Unknown").to_string(), + parts.first().copied().unwrap_or("Unknown").to_string(), + parts.get(1).copied().unwrap_or("Unknown").to_string(), + parts.get(2).copied().unwrap_or("Unknown").to_string(), ) - } - None => ( - "Unknown".to_string(), - "Unknown".to_string(), - "Unknown".to_string(), - ), - }; + }, + ); println!("cargo:rustc-env=BUILD_DATE={date}"); println!("cargo:rustc-env=BUILD_TIME={time}"); println!("cargo:rustc-env=BUILD_YEAR={year}"); + + generate_inc(); +} + +fn unknown() -> String { + "Unknown".to_string() +} + +fn generate_inc() { + use std::fs; + + let template_path = "include/env_samp.inc.in"; + let output_path = "include/env_samp.inc"; + + let template = fs::read_to_string(template_path) + .unwrap_or_else(|e| panic!("failed to read {template_path}: {e}")); + + let version = env!("CARGO_PKG_VERSION"); + let rendered = template.replace("{{VERSION}}", version); + + if fs::read_to_string(output_path).ok().as_deref() != Some(rendered.as_str()) { + fs::write(output_path, &rendered) + .unwrap_or_else(|e| panic!("failed to write {output_path}: {e}")); + } } diff --git a/changelog/v0.x.md b/changelog/v0.x.md new file mode 100644 index 0000000..f06b709 --- /dev/null +++ b/changelog/v0.x.md @@ -0,0 +1,24 @@ +# Changelog — v0.x + +## [0.1.0] — 2025/01/15 + +Initial release. SA-MP-only plugin (built against `ZOTTCE/samp-rs`), distributed under GPL-3.0-only. + +### Added + +- Natives: `Env(key, dest, type, dest_len)` and `EnvCount()`. The `type` argument accepts `ENV_STRING` (default), `ENV_INT`, `ENV_FLOAT` and `ENV_BOOL`. `Env` returns `true` only when the key exists and the value parses successfully for the requested type; otherwise it writes a zero-equivalent default and returns `false`. +- `.env` parser with: single quotes (literal), double quotes with `\n` / `\r` / `\t` / `\"` / `\\` escapes, inline `#` comments (require preceding whitespace), `export ` prefix skip, UTF-8 BOM stripping and duplicate-key "last wins" semantics. +- Bool parsing accepts (case-insensitive) `true`/`1`/`yes`/`on` and `false`/`0`/`no`/`off`. +- 1 MiB hard limit on the `.env` file — files above the limit are rejected with a warning at startup. +- Single read at `OnGameModeInit`. No runtime reload, no overhead per `Env()` call. +- UTF-8-safe string truncation: long values are cut at a `char` boundary so partial multi-byte sequences are never written into the destination buffer. +- Logger with startup banner (plugin name, version, author, repository, build date). Values are never logged. +- Pawn include `env_samp.inc` exposing the natives and the `ENV_*` type constants. +- Example gamemode in `examples/example.pwn` and a public `.env.example` template. +- Build script `scripts/build.sh` cross-compiling `env_samp.so` (Linux i686) and `env_samp.dll` (Windows i686, via `i686-pc-windows-gnu`), with SHA-256 checksums alongside each artifact. + +### Security + +- The `.env` is read **once** at startup. No runtime reload. +- Values are **never** logged. Only the count of loaded variables and file-level errors are emitted. +- Files larger than 1 MiB are rejected at load time. diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..b7dda9d --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,45 @@ +# API reference + +## Constants + +| Constant | Value | Used by | +|---|---|---| +| `ENV_STRING` | `0` | `Env(..., ENV_STRING, ...)` | +| `ENV_INT` | `1` | `Env(..., ENV_INT, ...)` | +| `ENV_FLOAT` | `2` | `Env(..., ENV_FLOAT, ...)` | +| `ENV_BOOL` | `3` | `Env(..., ENV_BOOL, ...)` | +| `ENV_SAMP_VERSION` | string | Compile-time version of the plugin (mirrors `Cargo.toml`). | + +## `Env` + +```pawn +native bool:Env(const key[], dest[], type = ENV_STRING, dest_len = sizeof(dest)); +``` + +Looks up `key` in the loaded `.env` map, converts it to `type` and writes it into `dest`. + +**Parameters** + +| Name | Description | +|---|---| +| `key` | Variable name to retrieve. | +| `dest` | Destination buffer. Type depends on `type`: `char[]` for `ENV_STRING`, `new var` for `ENV_INT`/`ENV_BOOL`, `new Float:var` for `ENV_FLOAT`. | +| `type` | One of `ENV_STRING`, `ENV_INT`, `ENV_FLOAT`, `ENV_BOOL`. Defaults to `ENV_STRING`. | +| `dest_len` | Size of `dest`. Auto-filled with `sizeof(dest)` when omitted. | + +**Returns** + +- `true` if `key` exists **and** the value parsed successfully for the requested type. +- `false` if the key is missing, the value is empty, or it fails the type conversion. In that case `dest` receives a zero-equivalent default (`0`, `0.0`, `false`, or an empty string). + +**Unknown type** + +If `type` is outside the supported range, the plugin logs a warning and falls back to `ENV_STRING`. + +## `EnvCount` + +```pawn +native EnvCount(); +``` + +Returns the total number of variables loaded from the `.env` file at startup. Returns `0` when the file is missing, empty, or unreadable. Useful for diagnostics during `OnGameModeInit`. diff --git a/docs/dotenv-format.md b/docs/dotenv-format.md new file mode 100644 index 0000000..23e0241 --- /dev/null +++ b/docs/dotenv-format.md @@ -0,0 +1,52 @@ +# .env format + +The parser follows the de-facto `.env` convention: one `KEY=VALUE` per line, with quoting, comments and escapes. + +## Basic rules + +- Empty lines and lines starting with `#` are ignored. +- Lines starting with `export ` are skipped (they are not assignments). +- The first `=` on the line separates the key from the value. Trailing `=` characters are part of the value. +- Whitespace around the key and around the value is trimmed. +- Duplicate keys: **last one wins**. + +```bash +APP_ENV=production +APP_DEBUG=false + +MYSQL_HOST = 127.0.0.1 +MYSQL_PORT = 3306 +EMPTY_VALUE = +``` + +## Quoting + +| Quote style | Behavior | +|---|---| +| `'single'` | Literal — no escape sequences are interpreted. | +| `"double"` | Supports `\n`, `\r`, `\t`, `\"`, `\\`, plus literal pass-through for any other escape. | +| unquoted | Trimmed; an inline `#` preceded by whitespace starts a comment. | + +```bash +LITERAL_TEXT = 'text with literal \n and \t' +SERVER_NAME = "My SA:MP Server" +MOTD = "Line 1\nLine 2" +WINDOWS_PATH = "C:\\samp\\plugins" +``` + +## Inline comments + +A `#` only starts a comment when preceded by whitespace. Without preceding whitespace it is part of the value. + +```bash +WITH_INLINE_COMMENT = value # this comment is ignored +HASH_WITHOUT_SPACE = value#this_is_part_of_the_value +``` + +## Encoding + +The file must be valid UTF-8. A leading BOM (`U+FEFF`) is stripped automatically. Files larger than 1 MiB are rejected with a warning. + +## Full example + +[`examples/env`](https://github.com/NullSablex/env_samp/blob/master/examples/env) in the repository walks through every syntactic feature shown above in a single annotated file — copy it to `.env`, edit the values, and you're set. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9d59710 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,37 @@ +# env_samp + +Environment variables (`.env`) plugin for SA-MP and Open Multiplayer, written entirely in Rust. Loads a `.env` file at startup and exposes its values to Pawn through a single typed native. + +The same `.so` / `.dll` runs on SA-MP and on Open Multiplayer — natively as a component (recommended) or via legacy mode. See [Installation](installation.md) for both registration paths. + +## Where to start + +| Goal | Path | +|---|---| +| First time here | [Installation](installation.md) → [Usage](usage.md) | +| Learning the file syntax | [.env format](dotenv-format.md) | +| Quick lookup | [API reference](api-reference.md) | + +## Minimal example + +```pawn +#include +#include + +public OnGameModeInit() +{ + new host[64]; + if (Env("MYSQL_HOST", host)) + { + printf("[env_samp] MYSQL_HOST=%s", host); + } + + new port; + if (Env("MYSQL_PORT", port, ENV_INT)) + { + printf("[env_samp] MYSQL_PORT=%d", port); + } + + return 1; +} +``` diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..2185273 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,47 @@ +# Installation + +Drop the platform-specific binary into your server's `plugins/` folder and register it. The same binaries work on SA-MP and Open Multiplayer. + +## Artifacts + +Each release at [github.com/NullSablex/env_samp/releases](https://github.com/NullSablex/env_samp/releases) ships: + +- `env_samp.so` — Linux i686 (`i686-unknown-linux-gnu`). +- `env_samp.dll` — Windows i686 (`i686-pc-windows-msvc`). +- `env_samp.inc` — Pawn include, identical for SA-MP and Open Multiplayer. + +## SA-MP + +1. Copy `env_samp.so` / `env_samp.dll` into `plugins/`. +2. Copy `env_samp.inc` into `pawno/include/`. +3. Register under `plugins=` in `server.cfg`: + +```ini +plugins env_samp +``` + +## Open Multiplayer (native component, recommended) + +Drop the binary into the server's `components/` folder. open.mp auto-discovers it on start and loads it via `ComponentEntryPoint`, with access to `ICore`, `ITimersComponent` and the other native APIs. **No `config.json` entry is required** — native components are discovered by scanning the folder, not by declaration. + +## Open Multiplayer (legacy mode) + +Drop the binary into `plugins/` and declare it under `pawn.legacy_plugins` in `config.json` (legacy plugins must be listed explicitly, unlike native components): + +```json +{ + "pawn": { + "legacy_plugins": ["env_samp"] + } +} +``` + +## .env file + +Place a `.env` file alongside your server executable. The plugin reads `./.env` on `OnGameModeInit`. See [.env format](dotenv-format.md) for the accepted syntax. + +A ready-made template lives in the repository at [`examples/env`](https://github.com/NullSablex/env_samp/blob/master/examples/env) — copy it to `.env` and replace the values. + +## Example gamemode + +A minimal Pawn script wiring every supported type lives at [`examples/example.pwn`](https://github.com/NullSablex/env_samp/blob/master/examples/example.pwn). It's also the script used to smoke-test the plugin during development. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..31cdfc8 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,13 @@ +# Build dependencies for the MkDocs Material documentation site. +# +# Pinned with `>=` so the docs workflow picks up patch and minor releases +# automatically (security fixes, new Material features). For a fully +# reproducible build, replace with exact `==` pins. +# +# Used by `.github/workflows/docs.yml` (which also reads this file as the +# `actions/setup-python` `cache: pip` key) and by anyone running +# `mkdocs serve` locally. + +mkdocs>=1.6 +mkdocs-material>=9.5 +pymdown-extensions>=10.7 diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..3657385 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,56 @@ +# Usage + +The plugin exposes two natives: `Env` to read values and `EnvCount` to inspect how many were loaded. + +## Reading by type + +`Env(key, dest, type, dest_len)` writes the value of `key` into `dest`, converted to `type`. It returns `true` if the key exists *and* the value parsed successfully for that type. + +```pawn +new host[64]; +if (Env("MYSQL_HOST", host)) +{ + printf("MYSQL_HOST=%s", host); +} + +new port; +if (Env("MYSQL_PORT", port, ENV_INT)) +{ + printf("MYSQL_PORT=%d", port); +} + +new Float:rate; +if (Env("TICK_RATE", rate, ENV_FLOAT)) +{ + printf("TICK_RATE=%f", rate); +} + +new bool:debug; +if (Env("APP_DEBUG", debug, ENV_BOOL)) +{ + printf("APP_DEBUG=%d", _:debug); +} +``` + +## Supported types + +| Constant | Meaning | +|---|---| +| `ENV_STRING` | Default. Writes the raw value into the buffer. | +| `ENV_INT` | Parses the value with `i32::from_str` (`base 10`). | +| `ENV_FLOAT` | Parses the value with `f32::from_str`. | +| `ENV_BOOL` | Accepts (case-insensitive) `true`/`1`/`yes`/`on` → true and `false`/`0`/`no`/`off` → false. | + +If the value is missing or fails to parse, `Env` writes a zero/empty default into `dest` and returns `false`. + +## Counting + +`EnvCount()` returns the number of variables loaded from the `.env` file. Useful for diagnostics during boot. + +```pawn +printf("[env_samp] Variables loaded: %d", EnvCount()); +``` + +## Full gamemode example + +A complete Pawn script exercising all four types and `EnvCount` lives at [`examples/example.pwn`](https://github.com/NullSablex/env_samp/blob/master/examples/example.pwn) in the repository, paired with [`examples/env`](https://github.com/NullSablex/env_samp/blob/master/examples/env) as a copy-and-edit `.env` template. diff --git a/.env.example b/examples/env similarity index 52% rename from .env.example rename to examples/env index 3b9dee1..e59ce8e 100644 --- a/.env.example +++ b/examples/env @@ -1,57 +1,57 @@ # ============================== -# env_samp - Exemplo de .env +# env_samp - .env example # ============================== -# Este arquivo demonstra TUDO que o parser aceita. +# This file demonstrates EVERYTHING the parser accepts. # ------------------------------ -# 1) Formato básico / espaços +# 1) Basic format / whitespace # ------------------------------ -# Sem espaços (também suportado) +# Without spaces (also supported) APP_ENV=production APP_DEBUG=false -# Com espaços (também suportado) +# With spaces (also supported) MYSQL_HOST = 127.0.0.1 MYSQL_PORT = 3306 MYSQL_USER = samp_user -MYSQL_PASS = 'senha forte aqui' +MYSQL_PASS = 'strong password here' MYSQL_DB = samp EMPTY_VALUE = # ------------------------------ -# 2) Aspas simples (literal) +# 2) Single quotes (literal) # ------------------------------ -LITERAL_TEXT = 'texto com \n e \t literal' +LITERAL_TEXT = 'text with literal \n and \t' # ------------------------------ -# 3) Aspas duplas (com escapes) +# 3) Double quotes (with escapes) # ------------------------------ -SERVER_NAME = "Meu Servidor SA:MP" -MOTD = "Linha 1\nLinha 2" -ESCAPED_QUOTES = "Ele disse: \"oi\"" +SERVER_NAME = "My SA:MP Server" +MOTD = "Line 1\nLine 2" +ESCAPED_QUOTES = "He said: \"hi\"" WINDOWS_PATH = "C:\\samp\\plugins" # ------------------------------ -# 4) Comentários +# 4) Comments # ------------------------------ -# Linha iniciada com # é ignorada +# Lines starting with # are ignored SERVER_MODE = roleplay -SERVER_LANGUAGE = pt_BR -WITH_INLINE_COMMENT = valor # este comentário é ignorado -HASH_WITHOUT_SPACE = valor#isto_faz_parte_do_valor +SERVER_LANGUAGE = en_US +WITH_INLINE_COMMENT = value # this comment is ignored +HASH_WITHOUT_SPACE = value#this_is_part_of_the_value # ------------------------------ -# 5) Primeiro '=' separa chave/valor +# 5) The first '=' separates key/value # ------------------------------ DATABASE_URL = mysql://user:pass@127.0.0.1:3306/samp?opt=a=b=c # ------------------------------ -# 6) Chave duplicada (a última vence) +# 6) Duplicate keys (last one wins) # ------------------------------ MAX_PLAYERS = 50 MAX_PLAYERS = 100 # ------------------------------ -# 7) Tokens / segredos +# 7) Tokens / secrets # ------------------------------ -API_TOKEN = "troque-este-token" +API_TOKEN = "replace-this-token" diff --git a/examples/example.pwn b/examples/example.pwn index 199c733..a4474b2 100644 --- a/examples/example.pwn +++ b/examples/example.pwn @@ -5,9 +5,9 @@ main() {} public OnGameModeInit() { - printf("[env_samp] Variaveis carregadas: %d", EnvCount()); + printf("[env_samp] Variables loaded: %d", EnvCount()); - // String (tipo padrao) + // String (default type) new host[64]; if (Env("MYSQL_HOST", host)) { @@ -28,7 +28,7 @@ public OnGameModeInit() printf("[env_samp] TICK_RATE=%f", rate); } - // Bool + // Boolean new bool:debug; if (Env("APP_DEBUG", debug, ENV_BOOL)) { diff --git a/pawn/include/env_samp.inc b/include/env_samp.inc similarity index 80% rename from pawn/include/env_samp.inc rename to include/env_samp.inc index 3424990..944df2f 100644 --- a/pawn/include/env_samp.inc +++ b/include/env_samp.inc @@ -1,7 +1,8 @@ /* - * env_samp - Environment Variables Plugin for SA:MP - * GNU General Public License v3.0 - * https://github.com/NullSablex/env_samp + * env_samp v1.0.0 - Environment Variables Plugin for SA-MP and Open Multiplayer. + * Author: NullSablex + * Repository: https://github.com/NullSablex/env_samp + * License: AGPL-3.0-or-later */ #if defined _env_samp_included @@ -9,6 +10,8 @@ #endif #define _env_samp_included +#define ENV_SAMP_VERSION "1.0.0" + /* * Type constants for Env() native */ diff --git a/include/env_samp.inc.in b/include/env_samp.inc.in new file mode 100644 index 0000000..c1f087c --- /dev/null +++ b/include/env_samp.inc.in @@ -0,0 +1,43 @@ +/* + * env_samp v{{VERSION}} - Environment Variables Plugin for SA-MP and Open Multiplayer. + * Author: NullSablex + * Repository: https://github.com/NullSablex/env_samp + * License: AGPL-3.0-or-later + */ + +#if defined _env_samp_included + #endinput +#endif +#define _env_samp_included + +#define ENV_SAMP_VERSION "{{VERSION}}" + +/* + * Type constants for Env() native + */ +#define ENV_STRING 0 /* Return value as string (default) */ +#define ENV_INT 1 /* Convert and return as integer */ +#define ENV_FLOAT 2 /* Convert and return as float */ +#define ENV_BOOL 3 /* Convert and return as boolean (0 or 1) */ + +/* + * Get environment variable from .env file + * + * Params: + * key: Variable name to retrieve + * dest: Buffer to store the result + * type: Data type (ENV_STRING, ENV_INT, ENV_FLOAT, ENV_BOOL) + * dest_len: Size of destination buffer (auto-calculated by default) + * + * Returns: + * true if variable exists and was retrieved, false otherwise + */ +native bool:Env(const key[], dest[], type = ENV_STRING, dest_len = sizeof(dest)); + +/* + * Get total number of variables loaded from .env + * + * Returns: + * Total count of environment variables + */ +native EnvCount(); diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a08b3e4 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,93 @@ +site_name: env_samp +site_description: Environment variables (.env) plugin for SA-MP and Open Multiplayer, written in Rust +site_author: NullSablex +site_url: https://env-samp.nullsablex.com/ + +repo_name: NullSablex/env_samp +repo_url: https://github.com/NullSablex/env_samp +edit_uri: edit/master/docs/ + +docs_dir: docs + +theme: + name: material + language: en + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.expand + - navigation.top + - navigation.tracking + - navigation.indexes + - toc.follow + - search.suggest + - search.highlight + - search.share + - content.code.copy + - content.code.annotate + - content.tabs.link + - content.action.edit + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: green + accent: light green + toggle: + icon: material/weather-night + name: Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: green + accent: light green + toggle: + icon: material/weather-sunny + name: Light mode + icon: + repo: fontawesome/brands/github + edit: material/pencil + view: material/eye + +plugins: + - search: + lang: en + +markdown_extensions: + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + permalink_title: Permanent link + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +nav: + - Home: index.md + - Guide: + - Installation: installation.md + - Usage: usage.md + - .env format: dotenv-format.md + - Reference: + - API reference: api-reference.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/NullSablex/env_samp diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh new file mode 100755 index 0000000..e1e7317 --- /dev/null +++ b/scripts/build-linux.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Builds the plugin for Linux and Windows from Linux. +# +# Outputs: +# dist/.so — Linux (i686-unknown-linux-gnu) +# dist/.dll — Windows (i686-pc-windows-msvc via cargo-xwin) +# +# Always builds with SA-MP + native Open Multiplayer support. +# Requires cargo-xwin (installed automatically if missing). +# +# Usage: +# ./scripts/build-linux.sh # release +# PROFILE=dev ./scripts/build-linux.sh # dev build +# +# PLUGIN_NAME is read from the project's Cargo.toml. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIST_DIR="$ROOT_DIR/dist" +PROFILE="${PROFILE:-release}" +PLUGIN_NAME="$(grep -m1 '^name' "$ROOT_DIR/Cargo.toml" | sed 's/.*= *"\(.*\)"/\1/' | tr '-' '_')" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[build] $*${NC}"; } +log_step() { echo -e "${YELLOW}[build] $*${NC}"; } +log_err() { echo -e "${RED}[build] $*${NC}" >&2; } + +for arg in "$@"; do + log_err "Unknown argument: $arg" + exit 1 +done + +ensure_target() { + if ! rustup target list --installed | grep -qx "$1"; then + log_step "Installing target: $1" + rustup target add "$1" + fi +} + +ensure_xwin() { + if ! command -v cargo-xwin >/dev/null 2>&1; then + log_step "Installing cargo-xwin..." + cargo install cargo-xwin + fi +} + +build_linux() { + local target="i686-unknown-linux-gnu" + ensure_target "$target" + log_step "Building: $target" + cargo build --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/lib${PLUGIN_NAME}.so" + local dst="$DIST_DIR/${PLUGIN_NAME}.so" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Linux: $dst" +} + +build_windows() { + local target="i686-pc-windows-msvc" + ensure_target "$target" + ensure_xwin + log_step "Building: $target" + cargo xwin build --xwin-arch x86 --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/${PLUGIN_NAME}.dll" + local dst="$DIST_DIR/${PLUGIN_NAME}.dll" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Windows: $dst" +} + +main() { + mkdir -p "$DIST_DIR" + log_info "Mode: SA-MP + native Open Multiplayer" + build_linux + build_windows + log_info "Done: $DIST_DIR/" +} + +main diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100755 index 0000000..1a9af56 --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Builds the plugin for Windows and Linux from Windows (Git Bash). +# +# Outputs: +# dist/.dll — Windows (i686-pc-windows-msvc) +# dist/.so — Linux (i686-unknown-linux-gnu via WSL or Docker/cross) +# +# Always builds with SA-MP + native Open Multiplayer support. +# +# Linux (.so) build: +# WSL — autodetected. Requires Rust inside WSL (https://rustup.rs). +# Docker — fallback when WSL is unavailable. Requires Docker Desktop + cross. +# Force a mode with --wsl or --docker. +# +# Usage: +# ./scripts/build-windows.sh # autodetect +# ./scripts/build-windows.sh --wsl # force WSL for Linux +# ./scripts/build-windows.sh --docker # force Docker for Linux +# PROFILE=dev ./scripts/build-windows.sh # dev build (default: release) +# +# PLUGIN_NAME is read from the project's Cargo.toml. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIST_DIR="$ROOT_DIR/dist" +PROFILE="${PROFILE:-release}" +PLUGIN_NAME="$(grep -m1 '^name' "$ROOT_DIR/Cargo.toml" | sed 's/.*= *"\(.*\)"/\1/' | tr '-' '_')" +LINUX_BUILD="" # "wsl" | "docker" | "" (autodetect) + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[build] $*${NC}"; } +log_step() { echo -e "${YELLOW}[build] $*${NC}"; } +log_err() { echo -e "${RED}[build] $*${NC}" >&2; } + +for arg in "$@"; do + case "$arg" in + --wsl) LINUX_BUILD="wsl" ;; + --docker) LINUX_BUILD="docker" ;; + *) log_err "Unknown argument: $arg"; exit 1 ;; + esac +done + +ensure_target() { + if ! rustup target list --installed | grep -qx "$1"; then + log_step "Installing target: $1" + rustup target add "$1" + fi +} + +ensure_cross() { + if ! command -v cross >/dev/null 2>&1; then + log_step "Installing cross..." + cargo install cross + fi +} + +build_windows() { + local target="i686-pc-windows-msvc" + ensure_target "$target" + log_step "Building: $target" + cargo build --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/${PLUGIN_NAME}.dll" + local dst="$DIST_DIR/${PLUGIN_NAME}.dll" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Windows: $dst" +} + +build_linux_wsl() { + local target="i686-unknown-linux-gnu" + local wsl_root="/mnt${ROOT_DIR}" + log_step "Building: $target (via WSL)" + wsl bash -c "rustup target add '$target' 2>/dev/null; cd '$wsl_root' && cargo build --profile '$PROFILE' --target '$target'" + + local src="$ROOT_DIR/target/$target/$PROFILE/lib${PLUGIN_NAME}.so" + local dst="$DIST_DIR/${PLUGIN_NAME}.so" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Linux: $dst" +} + +build_linux_docker() { + local target="i686-unknown-linux-gnu" + ensure_target "$target" + ensure_cross + log_step "Building: $target (via cross/Docker)" + cross build --profile "$PROFILE" --target "$target" + + local src="$ROOT_DIR/target/$target/$PROFILE/lib${PLUGIN_NAME}.so" + local dst="$DIST_DIR/${PLUGIN_NAME}.so" + [[ -f "$src" ]] || { log_err "Artifact not found: $src"; exit 1; } + cp "$src" "$dst" + log_info "Linux: $dst" +} + +build_linux() { + case "$LINUX_BUILD" in + wsl) build_linux_wsl ;; + docker) build_linux_docker ;; + *) + if command -v wsl >/dev/null 2>&1; then + log_step "WSL detected." + build_linux_wsl + elif command -v docker >/dev/null 2>&1; then + log_step "Docker detected." + build_linux_docker + else + log_err "Neither WSL nor Docker found. Install one or force a mode with --wsl/--docker." + exit 1 + fi + ;; + esac +} + +main() { + mkdir -p "$DIST_DIR" + log_info "Mode: SA-MP + native Open Multiplayer" + build_windows + build_linux + log_info "Done: $DIST_DIR/" +} + +main diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100755 index c0ef50c..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -DIST_DIR="$ROOT_DIR/dist" -PROFILE="${PROFILE:-release}" -LINUX_TARGET="i686-unknown-linux-gnu" -WINDOWS_TARGET="i686-pc-windows-gnu" -PLUGIN_NAME="env_samp" - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -log_info() { echo -e "${GREEN}$*${NC}"; } -log_step() { echo -e "${YELLOW}$*${NC}"; } -log_err() { echo -e "${RED}$*${NC}" >&2; } - -ensure_target() { - local target="$1" - if ! rustup target list --installed | grep -qx "$target"; then - log_step "Instalando target Rust: $target" - rustup target add "$target" - fi -} - -ensure_sha256() { - if command -v sha256sum >/dev/null 2>&1; then - return 0 - fi - if command -v shasum >/dev/null 2>&1; then - return 0 - fi - log_err "Nenhum utilitário de hash encontrado (sha256sum/shasum)." - exit 1 -} - -write_sha256() { - local file="$1" - local out="$2" - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$file" > "$out" - else - shasum -a 256 "$file" > "$out" - fi -} - -build_target() { - local target="$1" - log_step "Compilando target: $target" - cargo build --profile "$PROFILE" --target "$target" -} - -copy_linux() { - local src="$ROOT_DIR/target/$LINUX_TARGET/$PROFILE/lib${PLUGIN_NAME}.so" - local dst="$DIST_DIR/${PLUGIN_NAME}.so" - [[ -f "$src" ]] || { log_err "Artefato Linux não encontrado: $src"; exit 1; } - cp "$src" "$dst" - write_sha256 "$dst" "${dst}.sha256" -} - -copy_windows() { - local src="$ROOT_DIR/target/$WINDOWS_TARGET/$PROFILE/${PLUGIN_NAME}.dll" - local dst="$DIST_DIR/${PLUGIN_NAME}.dll" - [[ -f "$src" ]] || { log_err "Artefato Windows não encontrado: $src"; exit 1; } - cp "$src" "$dst" - write_sha256 "$dst" "${dst}.sha256" -} - -main() { - log_info "Build do plugin SA:MP env_samp (Linux + Windows 32-bit)" - mkdir -p "$DIST_DIR" - ensure_target "$LINUX_TARGET" - ensure_target "$WINDOWS_TARGET" - ensure_sha256 - build_target "$LINUX_TARGET" - build_target "$WINDOWS_TARGET" - copy_linux - copy_windows - log_info "Concluído. Arquivos gerados em: $DIST_DIR" -} - -main "$@" diff --git a/src/dotenv.rs b/src/dotenv.rs index 746b98a..5803b9a 100644 --- a/src/dotenv.rs +++ b/src/dotenv.rs @@ -11,6 +11,39 @@ pub enum DotenvError { Utf8, } +impl std::fmt::Display for DotenvError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotFound => write!(f, "not found"), + Self::NotRegularFile => write!(f, "is not a regular file"), + Self::TooLarge { max_bytes, actual_bytes } => write!( + f, + "exceeds the size limit ({actual_bytes} > {max_bytes} bytes)" + ), + Self::Io(err) => write!(f, "I/O error: {err}"), + Self::Utf8 => write!(f, "contains invalid UTF-8"), + } + } +} + +impl std::error::Error for DotenvError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(err) => Some(err), + _ => None, + } + } +} + +impl DotenvError { + pub const fn is_warning(&self) -> bool { + matches!( + self, + Self::NotFound | Self::NotRegularFile | Self::TooLarge { .. } + ) + } +} + pub fn load_dotenv_file( path: &Path, max_bytes: usize, @@ -40,58 +73,65 @@ pub fn load_dotenv_file( } let content = String::from_utf8(bytes).map_err(|_| DotenvError::Utf8)?; - let content = content.strip_prefix('\u{FEFF}').unwrap_or(&content); - Ok(parse_dotenv_str(content)) + Ok(parse_dotenv_str(&content)) } pub fn parse_dotenv_str(input: &str) -> HashMap { - let input = input.strip_prefix('\u{FEFF}').unwrap_or(input); let mut map = HashMap::new(); - - for raw_line in input.lines() { - let raw_line = raw_line.strip_suffix('\r').unwrap_or(raw_line); - let line = raw_line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; + for raw_line in strip_bom(input).lines() { + if let Some((key, value)) = parse_line(raw_line) { + map.insert(key, value); } - if line.starts_with("export ") { - continue; - } - let Some((key, rest)) = line.split_once('=') else { - continue; - }; + } + map +} - let key = key.trim(); - if key.is_empty() { - continue; - } +fn parse_line(raw: &str) -> Option<(String, String)> { + let line = raw.strip_suffix('\r').unwrap_or(raw).trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with("export ") { + return None; + } - let rest = rest.trim(); - let value = if rest.len() >= 2 && rest.starts_with('\'') && rest.ends_with('\'') { - rest[1..rest.len() - 1].to_string() - } else if rest.len() >= 2 && rest.starts_with('"') && rest.ends_with('"') { - unescape_double_quoted(&rest[1..rest.len() - 1]) - } else { - let mut cut: Option = None; - let mut prev_ws = false; - for (i, ch) in rest.char_indices() { - if ch == '#' && prev_ws { - cut = Some(i); - break; - } - prev_ws = ch.is_whitespace(); - } - let rest = match cut { - Some(i) => &rest[..i], - None => rest, - }; - rest.trim_end().to_string() - }; - - map.insert(key.to_string(), value); + let (key, rest) = line.split_once('=')?; + let key = key.trim(); + if key.is_empty() { + return None; } - map + Some((key.to_string(), parse_value(rest.trim()))) +} + +fn parse_value(rest: &str) -> String { + if let Some(inner) = strip_quoted(rest, '\'') { + return inner.to_string(); + } + if let Some(inner) = strip_quoted(rest, '"') { + return unescape_double_quoted(inner); + } + strip_inline_comment(rest).trim_end().to_string() +} + +fn strip_quoted(s: &str, quote: char) -> Option<&str> { + if s.len() >= 2 && s.starts_with(quote) && s.ends_with(quote) { + Some(&s[1..s.len() - 1]) + } else { + None + } +} + +fn strip_inline_comment(rest: &str) -> &str { + let mut prev_ws = false; + for (i, ch) in rest.char_indices() { + if ch == '#' && prev_ws { + return &rest[..i]; + } + prev_ws = ch.is_whitespace(); + } + rest +} + +fn strip_bom(input: &str) -> &str { + input.strip_prefix('\u{FEFF}').unwrap_or(input) } fn unescape_double_quoted(s: &str) -> String { @@ -107,8 +147,6 @@ fn unescape_double_quoted(s: &str) -> String { Some('n') => out.push('\n'), Some('r') => out.push('\r'), Some('t') => out.push('\t'), - Some('\\') => out.push('\\'), - Some('"') => out.push('"'), Some(other) => out.push(other), None => out.push('\\'), } diff --git a/src/env_type.rs b/src/env_type.rs new file mode 100644 index 0000000..3ee4f37 --- /dev/null +++ b/src/env_type.rs @@ -0,0 +1,48 @@ +//! Native parameter type tag and typed value parsers for each variant. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EnvType { + String, + Int, + Float, + Bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct UnknownEnvType(pub i32); + +impl std::fmt::Display for UnknownEnvType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "unknown env type: {}", self.0) + } +} + +impl TryFrom for EnvType { + type Error = UnknownEnvType; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(Self::String), + 1 => Ok(Self::Int), + 2 => Ok(Self::Float), + 3 => Ok(Self::Bool), + other => Err(UnknownEnvType(other)), + } + } +} + +pub fn parse_int(value: &str) -> Option { + value.trim().parse().ok() +} + +pub fn parse_float(value: &str) -> Option { + value.trim().parse().ok() +} + +pub fn parse_bool(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "true" | "1" | "yes" | "on" => Some(true), + "false" | "0" | "no" | "off" => Some(false), + _ => None, + } +} diff --git a/src/lib.rs b/src/lib.rs index e259841..f2ef963 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,126 +1,16 @@ mod dotenv; -mod log; -mod state; +mod env_type; +mod logger; +mod natives; +mod plugin; +mod store; -use samp::cell::string::put_in_buffer; -use samp::prelude::*; -use samp::{initialize_plugin, native}; -use std::path::Path; - -const DOTENV_PATH: &str = "./.env"; -const DOTENV_MAX_BYTES: usize = 1024 * 1024; - -#[allow(dead_code)] -const ENV_STRING: i32 = 0; -const ENV_INT: i32 = 1; -const ENV_FLOAT: i32 = 2; -const ENV_BOOL: i32 = 3; - -struct Plugin; - -impl SampPlugin for Plugin { - fn on_load(&mut self) { - let _ = log::init_logging(); - - if state::is_initialized() { - return; - } - - let path = Path::new(DOTENV_PATH); - match dotenv::load_dotenv_file(path, DOTENV_MAX_BYTES) { - Ok(map) => { - let count = map.len(); - let _ = state::init(map); - log::info(&format!("Carregadas {count} variavel(is) de {DOTENV_PATH}")); - } - Err(dotenv::DotenvError::NotFound) => { - let _ = state::init(Default::default()); - log::warn(&format!("{DOTENV_PATH} nao encontrado")); - } - Err(dotenv::DotenvError::NotRegularFile) => { - let _ = state::init(Default::default()); - log::warn(&format!("{DOTENV_PATH} nao e um arquivo regular")); - } - Err(dotenv::DotenvError::TooLarge { max_bytes, actual_bytes }) => { - let _ = state::init(Default::default()); - log::warn(&format!( - "{DOTENV_PATH} excede o limite ({actual_bytes} > {max_bytes} bytes)" - )); - } - Err(dotenv::DotenvError::Io(err)) => { - let _ = state::init(Default::default()); - log::error(&format!("Falha ao ler {DOTENV_PATH}: {err}")); - } - Err(dotenv::DotenvError::Utf8) => { - let _ = state::init(Default::default()); - log::error(&format!("{DOTENV_PATH} contem UTF-8 invalido")); - } - } - } -} - -impl Plugin { - #[native(name = "Env")] - fn env( - &mut self, - _amx: &Amx, - key: AmxString, - dest: UnsizedBuffer, - env_type: i32, - dest_len: i32, - ) -> AmxResult { - let key = key.to_string(); - let value = state::get(&key); - let buf_len = if dest_len > 0 { dest_len as usize } else { 1 }; - let mut buffer = dest.into_sized_buffer(buf_len); - - match env_type { - ENV_INT => { - let parsed = value.and_then(|v| v.trim().parse::().ok()); - buffer[0] = parsed.unwrap_or(0); - Ok(parsed.is_some()) - } - ENV_FLOAT => { - let parsed = value.and_then(|v| v.trim().parse::().ok()); - buffer[0] = parsed.unwrap_or(0.0).to_bits() as i32; - Ok(parsed.is_some()) - } - ENV_BOOL => { - let parsed = value.and_then(parse_bool); - buffer[0] = if parsed.unwrap_or(false) { 1 } else { 0 }; - Ok(parsed.is_some()) - } - _ => { - let out = value.unwrap_or(""); - let truncated = if out.len() >= buf_len { - &out[..out.floor_char_boundary(buf_len.saturating_sub(1))] - } else { - out - }; - put_in_buffer(&mut buffer, truncated)?; - Ok(value.is_some()) - } - } - } - - #[native(name = "EnvCount")] - fn env_count(&mut self, _amx: &Amx) -> AmxResult { - Ok(i32::try_from(state::count()).unwrap_or(i32::MAX)) - } -} - -fn parse_bool(value: &str) -> Option { - match value.trim().to_ascii_lowercase().as_str() { - "true" | "1" | "yes" | "on" => Some(true), - "false" | "0" | "no" | "off" => Some(false), - _ => None, - } -} +use plugin::EnvPlugin; +use samp::initialize_plugin; initialize_plugin!( - natives: [Plugin::env, Plugin::env_count], + natives: [EnvPlugin::env, EnvPlugin::env_count], { - let plugin = Plugin; - return plugin; + return EnvPlugin::new(); } ); diff --git a/src/log.rs b/src/log.rs deleted file mode 100644 index df3e79e..0000000 --- a/src/log.rs +++ /dev/null @@ -1,44 +0,0 @@ -pub fn init_logging() -> bool { - print_banner(); - true -} - -pub fn info(msg: &str) { - log::info!("{}", msg); -} - -pub fn warn(msg: &str) { - log::warn!("{}", msg); -} - -pub fn error(msg: &str) { - log::error!("{}", msg); -} - -fn print_banner() { - let name = env!("CARGO_PKG_NAME"); - let version = env!("CARGO_PKG_VERSION"); - let author = env!("CARGO_PKG_AUTHORS"); - let repository = env!("CARGO_PKG_REPOSITORY"); - let build_date = env!("BUILD_DATE"); - let build_time = env!("BUILD_TIME"); - let build_year = env!("BUILD_YEAR"); - - log::info!(""); - log::info!(" | {} {} | {}", name, version, build_year); - log::info!(" |-------------------------------"); - log::info!(" | Author and maintainer: {}", value_or(author, "Unknown")); - log::info!(""); - log::info!(" | Compiled: {} at {}", build_date, build_time); - log::info!(" |-------------------------------"); - log::info!(" | Repository: {}", value_or(repository, "N/A")); - log::info!(""); -} - -fn value_or<'a>(value: &'a str, fallback: &'a str) -> &'a str { - if value.trim().is_empty() { - fallback - } else { - value - } -} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..d2cd212 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,95 @@ +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + +const LOG_DIR: &str = "logs"; +const LOG_FILE: &str = "logs/env.log"; +const PREFIX: &str = "[Env]"; + +/// Log level: 0=none, 1=error, 2=warning, 3=info, 4=all (default) +static LOG_LEVEL: AtomicI32 = AtomicI32::new(4); + +static FILE_WRITE_REPORTED: AtomicBool = AtomicBool::new(false); + +pub struct Logger; + +impl Logger { + pub fn init() { + let _ = fs::create_dir_all(LOG_DIR); + Self::print_banner(); + } + + #[allow(dead_code)] + pub fn set_log_level(level: i32) { + LOG_LEVEL.store(level.clamp(0, 4), Ordering::Relaxed); + } + + #[allow(dead_code)] + pub fn info(msg: &str) { + if LOG_LEVEL.load(Ordering::Relaxed) >= 3 { + samp::log::info!("{PREFIX} {msg}"); + Self::write_file("INFO", msg); + } + } + + pub fn warn(msg: &str) { + if LOG_LEVEL.load(Ordering::Relaxed) >= 2 { + samp::log::warn!("{PREFIX} {msg}"); + Self::write_file("WARNING", msg); + } + } + + pub fn error(msg: &str) { + if LOG_LEVEL.load(Ordering::Relaxed) >= 1 { + samp::log::error!("{PREFIX} {msg}"); + Self::write_file("ERROR", msg); + } + } + + fn print_banner() { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + let author = value_or(env!("CARGO_PKG_AUTHORS"), "Unknown"); + let repository = value_or(env!("CARGO_PKG_REPOSITORY"), "N/A"); + let build_date = env!("BUILD_DATE"); + let build_time = env!("BUILD_TIME"); + let build_year = env!("BUILD_YEAR"); + + samp::log::info!(""); + samp::log::info!(" | {name} {version} | {build_year}"); + samp::log::info!(" |-------------------------------"); + samp::log::info!(" | Author and maintainer: {author}"); + samp::log::info!(""); + samp::log::info!(" | Compiled: {build_date} at {build_time}"); + samp::log::info!(" |-------------------------------"); + samp::log::info!(" | Repository: {repository}"); + samp::log::info!(""); + } + + fn write_file(level: &str, message: &str) { + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); + let line = format!("[{timestamp}] [{level}] {message}\n"); + + let result = OpenOptions::new() + .create(true) + .append(true) + .open(LOG_FILE) + .and_then(|mut file| file.write_all(line.as_bytes())); + + if let Err(err) = result + && !FILE_WRITE_REPORTED.swap(true, Ordering::Relaxed) + { + samp::log::error!( + "{PREFIX} Failed to write {LOG_FILE}: {err}. Further file-write errors will be suppressed." + ); + } + } +} + +fn value_or<'a>(value: &'a str, fallback: &'a str) -> &'a str { + if value.trim().is_empty() { + fallback + } else { + value + } +} diff --git a/src/natives.rs b/src/natives.rs new file mode 100644 index 0000000..d9e57a2 --- /dev/null +++ b/src/natives.rs @@ -0,0 +1,76 @@ +#![allow(clippy::needless_pass_by_ref_mut)] +// Natives must accept `&mut self` because the `#[native]` macro and the +// `SampPlugin` trait require that signature, even when the body never mutates. + +use samp::native; +use samp::prelude::*; + +use crate::env_type::{self, EnvType}; +use crate::logger::Logger; +use crate::plugin::EnvPlugin; + +impl EnvPlugin { + #[native(name = "Env")] + pub fn env( + &mut self, + _amx: &Amx, + key: &AmxString, + dest: UnsizedBuffer, + env_type_id: i32, + dest_len: i32, + ) -> AmxResult { + let key = key.to_string(); + let value = self.store.get(&key); + let buf_len = usize::try_from(dest_len).unwrap_or(0).max(1); + let kind = resolve_env_type(&key, env_type_id); + + match kind { + EnvType::String => write_string(dest, buf_len, value), + EnvType::Int => Ok(write_cell( + dest, + buf_len, + value.and_then(env_type::parse_int), + )), + EnvType::Float => Ok(write_cell( + dest, + buf_len, + value + .and_then(env_type::parse_float) + .map(|f| f.to_bits().cast_signed()), + )), + EnvType::Bool => Ok(write_cell( + dest, + buf_len, + value.and_then(env_type::parse_bool).map(i32::from), + )), + } + } + + #[native(name = "EnvCount")] + pub fn env_count(&mut self, _amx: &Amx) -> i32 { + i32::try_from(self.store.count()).unwrap_or(i32::MAX) + } +} + +fn resolve_env_type(key: &str, env_type_id: i32) -> EnvType { + match EnvType::try_from(env_type_id) { + Ok(kind) => kind, + Err(unknown) => { + Logger::warn(&format!( + "Env('{key}'): {unknown}, falling back to string" + )); + EnvType::String + } + } +} + +fn write_string(dest: UnsizedBuffer, buf_len: usize, value: Option<&str>) -> AmxResult { + dest.write_str(buf_len, value.unwrap_or(""))?; + Ok(value.is_some()) +} + +fn write_cell(dest: UnsizedBuffer, buf_len: usize, parsed: Option) -> bool { + let mut buffer = dest.into_sized_buffer(buf_len); + buffer.set_as::(0, parsed.unwrap_or(0)); + parsed.is_some() +} diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100644 index 0000000..c022544 --- /dev/null +++ b/src/plugin.rs @@ -0,0 +1,37 @@ +use samp::prelude::*; +use std::path::Path; + +use crate::dotenv; +use crate::logger::Logger; +use crate::store::EnvStore; + +pub const DOTENV_PATH: &str = "./.env"; +pub const DOTENV_MAX_BYTES: usize = 1024 * 1024; + +pub struct EnvPlugin { + pub store: EnvStore, +} + +impl EnvPlugin { + pub fn new() -> Self { + Logger::init(); + Self { + store: EnvStore::new(), + } + } +} + +impl SampPlugin for EnvPlugin { + fn on_load(&mut self) { + let path = Path::new(DOTENV_PATH); + match dotenv::load_dotenv_file(path, DOTENV_MAX_BYTES) { + Ok(map) => self.store.load(map), + Err(err) if err.is_warning() => { + Logger::warn(&format!("{DOTENV_PATH} {err}")); + } + Err(err) => { + Logger::error(&format!("{DOTENV_PATH}: {err}")); + } + } + } +} diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index 6a18f1c..0000000 --- a/src/state.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::collections::HashMap; -use std::sync::OnceLock; - -static ENV: OnceLock> = OnceLock::new(); - -pub fn init(map: HashMap) -> bool { - ENV.set(map).is_ok() -} - -pub fn get(key: &str) -> Option<&'static str> { - ENV.get().and_then(|m| m.get(key)).map(|s| s.as_str()) -} - -pub fn is_initialized() -> bool { - ENV.get().is_some() -} - -pub fn count() -> usize { - ENV.get().map_or(0, |m| m.len()) -} diff --git a/src/store.rs b/src/store.rs new file mode 100644 index 0000000..4d8ced8 --- /dev/null +++ b/src/store.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +#[derive(Default)] +pub struct EnvStore { + vars: HashMap, +} + +impl EnvStore { + pub fn new() -> Self { + Self::default() + } + + pub fn load(&mut self, map: HashMap) { + self.vars = map; + } + + pub fn get(&self, key: &str) -> Option<&str> { + self.vars.get(key).map(String::as_str) + } + + pub fn count(&self) -> usize { + self.vars.len() + } +} From 7077baafc51fec199d71fef1109e3344b59ceb23 Mon Sep 17 00:00:00 2001 From: NullSablex <244216261+NullSablex@users.noreply.github.com> Date: Mon, 25 May 2026 18:30:07 -0300 Subject: [PATCH 2/2] style: apply rustfmt to dotenv and natives - Expand the Self::TooLarge destructuring in DotenvError::fmt onto multiple lines, matching the default rustfmt struct-pattern width. - Collapse the Logger::warn call in resolve_env_type onto a single line. `cargo fmt --all -- --check` is now clean, unblocking the fmt CI job. --- src/dotenv.rs | 5 ++++- src/natives.rs | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dotenv.rs b/src/dotenv.rs index 5803b9a..7dbf807 100644 --- a/src/dotenv.rs +++ b/src/dotenv.rs @@ -16,7 +16,10 @@ impl std::fmt::Display for DotenvError { match self { Self::NotFound => write!(f, "not found"), Self::NotRegularFile => write!(f, "is not a regular file"), - Self::TooLarge { max_bytes, actual_bytes } => write!( + Self::TooLarge { + max_bytes, + actual_bytes, + } => write!( f, "exceeds the size limit ({actual_bytes} > {max_bytes} bytes)" ), diff --git a/src/natives.rs b/src/natives.rs index d9e57a2..568fa2b 100644 --- a/src/natives.rs +++ b/src/natives.rs @@ -56,9 +56,7 @@ fn resolve_env_type(key: &str, env_type_id: i32) -> EnvType { match EnvType::try_from(env_type_id) { Ok(kind) => kind, Err(unknown) => { - Logger::warn(&format!( - "Env('{key}'): {unknown}, falling back to string" - )); + Logger::warn(&format!("Env('{key}'): {unknown}, falling back to string")); EnvType::String } }