From 6e7fb54bcf44662f621c569a83567991444054d2 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:52:17 +0100 Subject: [PATCH] Fix + #154 --- AGENTS.md | 29 +++++++++++++ CONTRIBUTING.md | 4 ++ ci/expect_scripts/form-url-encoded.exp | 4 +- ci/expect_scripts/issue_154.exp | 28 +++++++++++++ flake.nix | 26 ++++++++---- platform/MultipartFormData.roc | 3 ++ tests/issue_154.roc | 58 ++++++++++++++++++++++++++ 7 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 AGENTS.md create mode 100644 ci/expect_scripts/issue_154.exp create mode 100644 tests/issue_154.roc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e410529 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ + +# General Info + +basic-webserver is a Roc platform: + +Every Roc application has exactly one platform. That platform provides all the I/O primitives that the application can use; Roc's standard library provides no I/O operations, and the only way for a Roc application to execute functions in other languages is if the platform offers a way to do that. + +Applications only interact with the Roc API portion of a platform, but there is also a host portion (written in a different language) that works behind the scenes. The host determines how the program starts, how memory is allocated and deallocated, and how I/O primitives are implemented. + +basic-webserver is implemented in Rust and Roc. + +# Useful Commands + +If you are in a nix dev shell, you can run `buildcmd` to build basic-webserver and `testcmd` to run all tests. Check if you are inside a nix shell with `echo $IN_NIX_SHELL`, enter one with `nix develop`. Or, run a command inside nix with `nix develop -c command` + +# Tests + +Note that if something is tested in ./examples, it may not have another test in ./tests. + +Run an individual test with: +``` +roc build --linker=legacy tests/issue_154.roc +TESTS_DIR=tests/ expect ci/expect_scripts/issue_154.exp +``` + +# Style + +- Prefer simple solutions. +- Try to achieve a single source of truth when sensible. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc0ad36..b0200a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,10 @@ We are committed to providing a friendly, safe and welcoming environment for all. Make sure to take a look at the Code of Conduct! +## Tips + +AGENTS.md is the place to find common commands and useful info. Handy for humans and AI agents. + ## How to generate docs? You can generate the documentation locally and then start a web server to host your files. diff --git a/ci/expect_scripts/form-url-encoded.exp b/ci/expect_scripts/form-url-encoded.exp index 70803dd..a3288e7 100644 --- a/ci/expect_scripts/form-url-encoded.exp +++ b/ci/expect_scripts/form-url-encoded.exp @@ -15,7 +15,7 @@ expect "Listening on \r\n" { set curlOutput [exec cat curl_form_output.txt] - if { [string match "*Form Data Received*" $curlOutput] && [string match "*Test+User*" $curlOutput] } { + if { [string match "*Form Data Received*" $curlOutput] && [string match "*Test User*" $curlOutput] } { exit 0 } else { puts "Error: curl output was different than expected: $curlOutput" @@ -24,4 +24,4 @@ expect "Listening on \r\n" { } puts stderr "\nError: output was different than expected." -exit 1 \ No newline at end of file +exit 1 diff --git a/ci/expect_scripts/issue_154.exp b/ci/expect_scripts/issue_154.exp new file mode 100644 index 0000000..0ce8263 --- /dev/null +++ b/ci/expect_scripts/issue_154.exp @@ -0,0 +1,28 @@ +#!/usr/bin/expect + +# uncomment line below for debugging +# exp_internal 1 + +set timeout 7 + +source ./ci/expect_scripts/shared-code.exp + +spawn $env(TESTS_DIR)issue_154 + +set expected_output [normalize_output " +Testing parse_form_url_encoded preserves literal plus signs. +Encoded input: message=This\\+%2B\\+is\\+a\\+plus +Decoded message: This \\+ is a plus +Expected message: This \\+ is a plus +Message decoded as expected. +Ran all tests. +"] + +expect -re $expected_output { + expect eof { + check_exit_and_segfault + } +} + +puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug." +exit 1 diff --git a/flake.nix b/flake.nix index 1c08192..0d1f9f5 100644 --- a/flake.nix +++ b/flake.nix @@ -29,9 +29,16 @@ rust = pkgs.rust-bin.fromRustupToolchainFile "${toString ./rust-toolchain.toml}"; - aliases = '' - alias buildcmd='bash jump-start.sh && roc ./build.roc -- --roc roc' - alias testcmd='export ROC=roc && export EXAMPLES_DIR=./examples/ && ./ci/all_tests.sh' + shellFunctions = '' + buildcmd() { + bash jump-start.sh && roc ./build.roc -- --roc roc "$@" + } + export -f buildcmd + + testcmd() { + export EXAMPLES_DIR=./examples/ && ./ci/all_tests.sh "$@" + } + export -f testcmd ''; linuxInputs = with pkgs; @@ -64,14 +71,19 @@ if pkgs.stdenv.isLinux then "${pkgs.glibc.out}/lib" else ""; shellHook = '' - ${aliases} + export ROC=roc + + ${shellFunctions} - echo "Some convenient command aliases:" - echo "${aliases}" | grep -E "alias .*" -o | sed 's/alias / /' | sed 's/=/ = /' + echo "Some convenient command functions:" + echo "${shellFunctions}" | grep -E '^\s*[a-zA-Z_][a-zA-Z0-9_]*\(\)' | sed 's/().*//' | sed 's/^[[:space:]]*/ /' | while read func; do + body=$(echo "${shellFunctions}" | sed -n "/''${func}()/,/^[[:space:]]*}/p" | sed '1d;$d' | tr '\n' ';' | sed 's/;$//' | sed 's/[[:space:]]*$//') + echo " $func = $body" + done echo "" ''; }; formatter = pkgs.nixpkgs-fmt; }); -} +} \ No newline at end of file diff --git a/platform/MultipartFormData.roc b/platform/MultipartFormData.roc index d29d4ee..4ac8d3f 100644 --- a/platform/MultipartFormData.roc +++ b/platform/MultipartFormData.roc @@ -375,6 +375,9 @@ parse_form_url_encoded = |bytes| help(tail, ParsingKey, [], [], Dict.insert(dict, key_str, value_str)), ), ) + ['+', ..] -> + # '+' is a space in application/x-www-form-urlencoded payloads + help(tail, state, key, List.append(chomped, ' '), dict) ['%', second_byte, third_byte, ..] -> hex = Num.to_u8(hex_bytes_to_u32([second_byte, third_byte])) diff --git a/tests/issue_154.roc b/tests/issue_154.roc new file mode 100644 index 0000000..7333dff --- /dev/null +++ b/tests/issue_154.roc @@ -0,0 +1,58 @@ +app [Model, init!, respond!] { + pf: platform "../platform/main.roc", +} + +import pf.Stdout +import pf.Http exposing [Request, Response] +import pf.MultipartFormData + +Model : {} + +init! : {} => Result Model _ +init! = |{}| + when run_tests!({}) is + Ok(_) -> + Err(Exit(0, "Ran all tests.")) + + Err(err) -> + Err(Exit(1, "Test run failed:\n\t${Inspect.to_str(err)}")) + +run_tests! : {} => Result {} _ +run_tests! = |{}| + Stdout.line!("Testing parse_form_url_encoded preserves literal plus signs.")? + + encoded = "message=This+%2B+is+a+plus" + Stdout.line!("Encoded input: ${encoded}")? + + dict = + ( + encoded + |> Str.to_utf8 + |> MultipartFormData.parse_form_url_encoded + )? + + message = + Dict.get(dict, "message")? + + Stdout.line!("Decoded message: ${message}")? + + expected = "This + is a plus" + Stdout.line!("Expected message: ${expected}")? + + if message == expected then + Stdout.line!("Message decoded as expected.")? + Ok({}) + else + Stdout.line!("Message decoded incorrectly.")? + + Err(StrErr("Decoded message mismatch: expected '${expected}' but got '${message}'.")) + +respond! : Request, Model => Result Response _ +respond! = |_, _| + Ok( + { + status: 404, + headers: [], + body: [], + }, + )