Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions aver-rt/src/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ pub fn send(host: &str, port: i64, message: &str) -> Result<String, String> {
Ok(String::from_utf8_lossy(&buf).into_owned())
}

/// Byte-clean sibling of [`send`].
///
/// Identical socket behaviour — open, write, `shutdown(Write)`, read to EOF —
/// but the payload and the response stay `Vec<u8>` end to end. `send` converts
/// the response with `String::from_utf8_lossy`, which replaces every non-UTF-8
/// sequence with U+FFFD and cannot be undone; that makes it unusable for binary
/// protocols whose framing bytes are not valid UTF-8. This function performs no
/// encoding or decoding, so the caller sees exactly what the peer sent.
pub fn send_bytes(host: &str, port: i64, payload: &[u8]) -> Result<Vec<u8>, String> {
validate_port(port)?;
let socket_addr = resolve(&format!("{}:{}", host, port))?;
let mut stream =
TcpStream::connect_timeout(&socket_addr, CONNECT_TIMEOUT).map_err(|e| e.to_string())?;
stream.set_read_timeout(Some(IO_TIMEOUT)).ok();
stream.set_write_timeout(Some(IO_TIMEOUT)).ok();
stream.write_all(payload).map_err(|e| e.to_string())?;
stream.shutdown(std::net::Shutdown::Write).ok();

let mut buf = Vec::new();
Read::by_ref(&mut stream)
.take(BODY_LIMIT as u64 + 1)
.read_to_end(&mut buf)
.map_err(|e| e.to_string())?;
if buf.len() > BODY_LIMIT {
return Err("Tcp.sendBytes: response exceeds 10 MB limit".to_string());
}
Ok(buf)
}

pub fn ping(host: &str, port: i64) -> Result<(), String> {
validate_port(port)?;
let socket_addr = resolve(&format!("{}:{}", host, port))?;
Expand Down
10 changes: 10 additions & 0 deletions docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ Source: `src/services/tcp.rs`
| Function | Signature |
|---|---|
| `Tcp.send` | `(String, Int, String) -> Result<String, String>` |
| `Tcp.sendBytes` | `(String, Int, List<Int>) -> Result<List<Int>, String>` |
| `Tcp.ping` | `(String, Int) -> Result<Unit, String>` |

**Persistent connections:**
Expand All @@ -272,6 +273,15 @@ Source: `src/services/tcp.rs`

`Tcp.send` is stateless and ephemeral — it opens a fresh socket, writes the request bytes raw (no `\r\n` append), `shutdown(Write)` to signal end-of-request, then reads the peer's response until EOF, capped at 10 MiB. It does **not** touch the persistent-connection pool, so a program holding 256 live `Tcp.connect` handles can still issue `Tcp.send` to another peer. Stream errors (`stream-error.last-operation-failed`) surface as `Result.Err("tcp: stream error")`; a clean half-close (`stream-error.closed`) returns whatever the peer flushed.

`Tcp.sendBytes` is the byte-clean form of `Tcp.send`: same socket behaviour, but
the payload and response stay `List<Int>` and no UTF-8 encoding or decoding
happens in either direction. Prefer it for any binary protocol. `Tcp.send`
decodes the response with `String::from_utf8_lossy`, which replaces every
non-UTF-8 sequence with U+FFFD — silently, irreversibly, and starting at the
first offending byte — so it is only safe for protocols whose responses are
valid UTF-8 text. Payload values outside `0..=255` return
`Result.Err` naming the offending value and its index.

### `Random` namespace — use granular effects (`! [Random.int]`, `! [Random.float]`)

Source: `src/services/random.rs` (backed by `aver_rt::random`)
Expand Down
116 changes: 112 additions & 4 deletions src/services/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
///
/// One-shot methods:
/// `Tcp.send(host, port, message)` — connect, write message, read response, close.
/// `Tcp.sendBytes(host, port, payload)` — same, but byte-clean: `List<Int>` in,
/// `List<Int>` out, no UTF-8 encoding or decoding on either side. `send`
/// decodes the response with `String::from_utf8_lossy`, which destroys any
/// non-UTF-8 byte irrecoverably; binary protocols need this variant.
/// `Tcp.ping(host, port)` — check whether the port accepts connections.
///
/// Persistent-connection methods:
Expand All @@ -14,14 +18,22 @@
use std::collections::HashMap;
use std::sync::Arc as Rc;

use aver_rt::TcpConnection;
use aver_rt::{AverList, TcpConnection};

use crate::nan_value::{Arena, NanValue, NanValueConvert};
use crate::value::{RuntimeError, Value};

pub fn register(global: &mut HashMap<String, Value>) {
let mut members = HashMap::new();
for method in &["send", "ping", "connect", "writeLine", "readLine", "close"] {
for method in &[
"send",
"sendBytes",
"ping",
"connect",
"writeLine",
"readLine",
"close",
] {
members.insert(
method.to_string(),
Value::Builtin(format!("Tcp.{}", method)),
Expand All @@ -38,6 +50,7 @@ pub fn register(global: &mut HashMap<String, Value>) {

pub const DECLARED_EFFECTS: &[&str] = &[
"Tcp.send",
"Tcp.sendBytes",
"Tcp.ping",
"Tcp.connect",
"Tcp.writeLine",
Expand All @@ -48,6 +61,7 @@ pub const DECLARED_EFFECTS: &[&str] = &[
pub fn effects(name: &str) -> &'static [&'static str] {
match name {
"Tcp.send" => &["Tcp.send"],
"Tcp.sendBytes" => &["Tcp.sendBytes"],
"Tcp.ping" => &["Tcp.ping"],
"Tcp.connect" => &["Tcp.connect"],
"Tcp.writeLine" => &["Tcp.writeLine"],
Expand All @@ -61,6 +75,7 @@ pub fn effects(name: &str) -> &'static [&'static str] {
pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
match name {
"Tcp.send" => Some(tcp_send(args)),
"Tcp.sendBytes" => Some(tcp_send_bytes(args)),
"Tcp.ping" => Some(tcp_ping(args)),
"Tcp.connect" => Some(tcp_connect(args)),
"Tcp.writeLine" => Some(tcp_write_line(args)),
Expand All @@ -87,6 +102,29 @@ fn tcp_send(args: &[Value]) -> Result<Value, RuntimeError> {
}
}

fn tcp_send_bytes(args: &[Value]) -> Result<Value, RuntimeError> {
if args.len() != 3 {
return Err(RuntimeError::Error(format!(
"Tcp.sendBytes() takes 3 arguments (host, port, payload), got {}",
args.len()
)));
}
let host = str_arg(&args[0], "Tcp.sendBytes: host must be a String")?;
let port = int_arg(&args[1], "Tcp.sendBytes: port must be an Int")?;
let payload = match bytes_arg(&args[2], "Tcp.sendBytes")? {
Ok(bytes) => bytes,
// Out-of-range byte values are a value error, not a type error, so they
// surface as a catchable `Result.Err` — same treatment the port range
// gets in `aver-rt::tcp` rather than a VM-only trap.
Err(msg) => return Ok(Value::Err(Box::new(Value::Str(msg)))),
};

match aver_rt::tcp::send_bytes(&host, port, &payload) {
Ok(response) => Ok(Value::Ok(Box::new(bytes_to_value(&response)))),
Err(e) => Ok(Value::Err(Box::new(Value::Str(e)))),
}
}

fn tcp_ping(args: &[Value]) -> Result<Value, RuntimeError> {
if args.len() != 2 {
return Err(RuntimeError::Error(format!(
Expand Down Expand Up @@ -225,6 +263,62 @@ fn str_arg(val: &Value, msg: &str) -> Result<String, RuntimeError> {
}
}

/// Convert a `List<Int>` argument into raw bytes.
///
/// The outer `Result` is the type check (wrong shape is a `RuntimeError`); the
/// inner one is the value check (an Int outside `0..=255` is a catchable Aver
/// `Result.Err`, reported with its index so a long payload is debuggable).
#[allow(clippy::type_complexity)]
fn bytes_arg(val: &Value, method: &str) -> Result<Result<Vec<u8>, String>, RuntimeError> {
let items = match val {
Value::List(items) => items,
_ => {
return Err(RuntimeError::Error(format!(
"{}: payload must be a List<Int>",
method
)));
}
};
let mut out = Vec::with_capacity(items.len());
for (idx, item) in items.iter().enumerate() {
let n = match item {
Value::Int(n) => match n.to_i64() {
Some(n) => n,
// An `Int` outside `i64` is still an `Int` — a fortiori out
// of byte range, so it takes the catchable value-error path,
// not the type-error one.
None => {
return Ok(Err(format!(
"{}: byte {} at index {} is out of range (0\u{2013}255)",
method, n, idx
)));
}
},
_ => {
return Err(RuntimeError::Error(format!(
"{}: payload must be a List<Int>",
method
)));
}
};
match u8::try_from(n) {
Ok(b) => out.push(b),
Err(_) => {
return Ok(Err(format!(
"{}: byte {} at index {} is out of range (0\u{2013}255)",
method, n, idx
)));
}
}
}
Ok(Ok(out))
}

fn bytes_to_value(bytes: &[u8]) -> Value {
let items: Vec<Value> = bytes.iter().map(|b| Value::int(*b as i64)).collect();
Value::List(AverList::from_vec(items))
}

fn int_arg(val: &Value, msg: &str) -> Result<i64, RuntimeError> {
// Phase 4.7+ fix #13 — type check only; the port-range check
// moved into `aver-rt::tcp::{connect, send, ping}` so every
Expand All @@ -242,7 +336,15 @@ fn int_arg(val: &Value, msg: &str) -> Result<i64, RuntimeError> {
// ─── NanValue-native API ─────────────────────────────────────────────────────

pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
let methods = &["send", "ping", "connect", "writeLine", "readLine", "close"];
let methods = &[
"send",
"sendBytes",
"ping",
"connect",
"writeLine",
"readLine",
"close",
];
let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
for method in methods {
let idx = arena.push_builtin(&format!("Tcp.{}", method));
Expand All @@ -263,7 +365,13 @@ pub fn call_nv(
) -> Option<Result<NanValue, RuntimeError>> {
if !matches!(
name,
"Tcp.send" | "Tcp.ping" | "Tcp.connect" | "Tcp.writeLine" | "Tcp.readLine" | "Tcp.close"
"Tcp.send"
| "Tcp.sendBytes"
| "Tcp.ping"
| "Tcp.connect"
| "Tcp.writeLine"
| "Tcp.readLine"
| "Tcp.close"
) {
return None;
}
Expand Down
10 changes: 10 additions & 0 deletions src/types/checker/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ impl TypeChecker {
"Env.set".to_string(),
"Tcp.connect".to_string(),
"Tcp.send".to_string(),
"Tcp.sendBytes".to_string(),
"Tcp.ping".to_string(),
"Tcp.writeLine".to_string(),
"Tcp.readLine".to_string(),
Expand Down Expand Up @@ -287,6 +288,15 @@ impl TypeChecker {
Type::Result(Box::new(Type::Str), Box::new(Type::Str)),
&["Tcp.send"],
),
(
"Tcp.sendBytes",
&[Type::Str, Type::Int, Type::List(Box::new(Type::Int))],
Type::Result(
Box::new(Type::List(Box::new(Type::Int))),
Box::new(Type::Str),
),
&["Tcp.sendBytes"],
),
(
"Tcp.ping",
&[Type::Str, Type::Int],
Expand Down
15 changes: 15 additions & 0 deletions src/types/checker/effect_classification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ pub enum RuntimeType {
TcpConnection,
/// `Result<Tcp.Connection, Str>` — return of `Tcp.connect`.
ResultTcpConnectionStr,
/// `List<Int>` — the byte payload argument on `Tcp.sendBytes`.
ListInt,
/// `Result<List<Int>, Str>` — return of `Tcp.sendBytes`.
ResultListIntStr,
}

impl RuntimeType {
Expand Down Expand Up @@ -111,6 +115,11 @@ impl RuntimeType {
RuntimeType::ResultTcpConnectionStr => {
Type::Result(Box::new(Type::named("Tcp.Connection")), Box::new(Type::Str))
}
RuntimeType::ListInt => Type::List(Box::new(Type::Int)),
RuntimeType::ResultListIntStr => Type::Result(
Box::new(Type::List(Box::new(Type::Int))),
Box::new(Type::Str),
),
}
}
}
Expand Down Expand Up @@ -284,6 +293,12 @@ const CLASSIFICATIONS: &[EffectClassification] = &[
runtime_params: &[RuntimeType::Str, RuntimeType::Int, RuntimeType::Str],
runtime_return: RuntimeType::ResultStrStr,
},
EffectClassification {
method: "Tcp.sendBytes",
dimension: EffectDimension::GenerativeOutput,
runtime_params: &[RuntimeType::Str, RuntimeType::Int, RuntimeType::ListInt],
runtime_return: RuntimeType::ResultListIntStr,
},
EffectClassification {
method: "Tcp.ping",
dimension: EffectDimension::GenerativeOutput,
Expand Down
3 changes: 3 additions & 0 deletions src/vm/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ vm_builtins! {
RandomFloat => "Random.float",

TcpSend => "Tcp.send",
TcpSendBytes => "Tcp.sendBytes",
TcpPing => "Tcp.ping",
TcpConnect => "Tcp.connect",
TcpWriteLine => "Tcp.writeLine",
Expand Down Expand Up @@ -274,6 +275,7 @@ impl VmBuiltin {
Self::RandomInt | Self::RandomFloat => random::effects(self.name()),

Self::TcpSend
| Self::TcpSendBytes
| Self::TcpPing
| Self::TcpConnect
| Self::TcpWriteLine
Expand Down Expand Up @@ -358,6 +360,7 @@ impl VmBuiltin {
Self::RandomInt | Self::RandomFloat => random::call_nv(self.name(), args, arena),

Self::TcpSend
| Self::TcpSendBytes
| Self::TcpPing
| Self::TcpConnect
| Self::TcpWriteLine
Expand Down
Loading
Loading