@tishlang/tish-format 1.0.13 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +2 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +10 -2
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +61 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
- package/README.md +0 -138
|
@@ -203,13 +203,13 @@ fn row_to_value_direct(row: &Row) -> tishlang_runtime::Value {
|
|
|
203
203
|
.try_get::<_, Option<&str>>(i)
|
|
204
204
|
.ok()
|
|
205
205
|
.flatten()
|
|
206
|
-
.map(|s| RtValue::String(
|
|
206
|
+
.map(|s| RtValue::String(tishlang_runtime::ArcStr::from(s)))
|
|
207
207
|
.unwrap_or(RtValue::Null),
|
|
208
208
|
_ => {
|
|
209
209
|
// Anything else goes through the JSON path for backwards
|
|
210
210
|
// compat. Hot TFB rows never hit this branch.
|
|
211
211
|
if let Ok(s) = row.try_get::<_, Option<String>>(i) {
|
|
212
|
-
s.map(|s| RtValue::String(
|
|
212
|
+
s.map(|s| RtValue::String(tishlang_runtime::ArcStr::from(s.as_str())))
|
|
213
213
|
.unwrap_or(RtValue::Null)
|
|
214
214
|
} else {
|
|
215
215
|
RtValue::Null
|
|
@@ -853,7 +853,7 @@ mod tish_sync {
|
|
|
853
853
|
return tish_err("migrate: unknown client handle");
|
|
854
854
|
};
|
|
855
855
|
|
|
856
|
-
let dir = std::path::PathBuf::from(dir.
|
|
856
|
+
let dir = std::path::PathBuf::from(dir.as_str());
|
|
857
857
|
let entries = match std::fs::read_dir(&dir) {
|
|
858
858
|
Ok(e) => e,
|
|
859
859
|
Err(e) => return tish_err(format!("migrate: read_dir({:?}): {e}", dir)),
|
|
@@ -151,6 +151,11 @@ fn collect_stmt(
|
|
|
151
151
|
collect_stmt(s, source, lsp_line, lsp_char, best);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
Statement::Multi { statements, .. } => {
|
|
155
|
+
for s in statements {
|
|
156
|
+
collect_stmt(s, source, lsp_line, lsp_char, best);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
154
159
|
Statement::FunDecl {
|
|
155
160
|
name,
|
|
156
161
|
name_span,
|
|
@@ -306,18 +311,16 @@ fn collect_destruct_pattern(
|
|
|
306
311
|
) {
|
|
307
312
|
match p {
|
|
308
313
|
DestructPattern::Array(elements) => {
|
|
309
|
-
for el in elements {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
consider(source, lsp_line, lsp_char, sp, n.clone(), best);
|
|
320
|
-
}
|
|
314
|
+
for el in elements.iter().flatten() {
|
|
315
|
+
match el {
|
|
316
|
+
DestructElement::Ident(n, sp) => {
|
|
317
|
+
consider(source, lsp_line, lsp_char, sp, n.clone(), best);
|
|
318
|
+
}
|
|
319
|
+
DestructElement::Pattern(inner) => {
|
|
320
|
+
collect_destruct_pattern(inner, source, lsp_line, lsp_char, best);
|
|
321
|
+
}
|
|
322
|
+
DestructElement::Rest(n, sp) => {
|
|
323
|
+
consider(source, lsp_line, lsp_char, sp, n.clone(), best);
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
326
|
}
|
|
@@ -484,11 +487,8 @@ fn collect_expr(
|
|
|
484
487
|
} => {
|
|
485
488
|
for p in props {
|
|
486
489
|
match p {
|
|
487
|
-
tishlang_ast::JsxProp::Attr { value, .. } =>
|
|
488
|
-
|
|
489
|
-
collect_expr(e, source, lsp_line, lsp_char, best)
|
|
490
|
-
}
|
|
491
|
-
_ => {}
|
|
490
|
+
tishlang_ast::JsxProp::Attr { value, .. } => if let tishlang_ast::JsxAttrValue::Expr(e) = value {
|
|
491
|
+
collect_expr(e, source, lsp_line, lsp_char, best)
|
|
492
492
|
},
|
|
493
493
|
tishlang_ast::JsxProp::Spread(e) => {
|
|
494
494
|
collect_expr(e, source, lsp_line, lsp_char, best)
|
|
@@ -784,6 +784,7 @@ fn member_chain_collect_fun_param(
|
|
|
784
784
|
}
|
|
785
785
|
}
|
|
786
786
|
|
|
787
|
+
#[allow(clippy::only_used_in_recursion)] // params threaded to recurse the pattern tree (mirrors sibling collectors)
|
|
787
788
|
fn member_chain_collect_destruct_pattern(
|
|
788
789
|
pattern: &DestructPattern,
|
|
789
790
|
source: &str,
|
|
@@ -793,15 +794,13 @@ fn member_chain_collect_destruct_pattern(
|
|
|
793
794
|
) {
|
|
794
795
|
match pattern {
|
|
795
796
|
DestructPattern::Array(elements) => {
|
|
796
|
-
for el in elements {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
DestructElement::Rest(_, _) => {}
|
|
804
|
-
}
|
|
797
|
+
for el in elements.iter().flatten() {
|
|
798
|
+
match el {
|
|
799
|
+
DestructElement::Ident(_, _) => {}
|
|
800
|
+
DestructElement::Pattern(inner) => member_chain_collect_destruct_pattern(
|
|
801
|
+
inner, source, lsp_line, lsp_char, best,
|
|
802
|
+
),
|
|
803
|
+
DestructElement::Rest(_, _) => {}
|
|
805
804
|
}
|
|
806
805
|
}
|
|
807
806
|
}
|
|
@@ -887,6 +886,11 @@ fn member_chain_collect_stmt(
|
|
|
887
886
|
member_chain_collect_stmt(s, source, lsp_line, lsp_char, best);
|
|
888
887
|
}
|
|
889
888
|
}
|
|
889
|
+
Statement::Multi { statements, .. } => {
|
|
890
|
+
for s in statements {
|
|
891
|
+
member_chain_collect_stmt(s, source, lsp_line, lsp_char, best);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
890
894
|
Statement::FunDecl { params, body, .. } => {
|
|
891
895
|
for p in params {
|
|
892
896
|
member_chain_collect_fun_param(p, source, lsp_line, lsp_char, best);
|
|
@@ -993,13 +997,11 @@ fn define_fun_param_stack(p: &FunParam, scopes: &mut ScopeStack) {
|
|
|
993
997
|
fn define_pattern_stack(pattern: &DestructPattern, scopes: &mut ScopeStack) {
|
|
994
998
|
match pattern {
|
|
995
999
|
DestructPattern::Array(elements) => {
|
|
996
|
-
for el in elements {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
DestructElement::Rest(n, sp) => scopes.define(n.as_ref(), *sp),
|
|
1002
|
-
}
|
|
1000
|
+
for el in elements.iter().flatten() {
|
|
1001
|
+
match el {
|
|
1002
|
+
DestructElement::Ident(n, sp) => scopes.define(n.as_ref(), *sp),
|
|
1003
|
+
DestructElement::Pattern(inner) => define_pattern_stack(inner, scopes),
|
|
1004
|
+
DestructElement::Rest(n, sp) => scopes.define(n.as_ref(), *sp),
|
|
1003
1005
|
}
|
|
1004
1006
|
}
|
|
1005
1007
|
}
|
|
@@ -1310,6 +1312,17 @@ fn walk_stmt_resolve(
|
|
|
1310
1312
|
scopes.pop();
|
|
1311
1313
|
out
|
|
1312
1314
|
}
|
|
1315
|
+
// Transparent group (comma-declarators): same scope, no push/pop.
|
|
1316
|
+
Statement::Multi { statements, .. } => {
|
|
1317
|
+
let mut out = None;
|
|
1318
|
+
for s in statements {
|
|
1319
|
+
if let Some(x) = walk_stmt_resolve(s, scopes, target) {
|
|
1320
|
+
out = Some(x);
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
out
|
|
1325
|
+
}
|
|
1313
1326
|
Statement::FunDecl {
|
|
1314
1327
|
name,
|
|
1315
1328
|
name_span,
|
|
@@ -1525,7 +1538,17 @@ pub fn is_runtime_global_ident(name: &str) -> bool {
|
|
|
1525
1538
|
| "Array"
|
|
1526
1539
|
| "String"
|
|
1527
1540
|
| "Date"
|
|
1541
|
+
| "Set"
|
|
1542
|
+
| "Map"
|
|
1543
|
+
| "Float64Array"
|
|
1544
|
+
| "Float32Array"
|
|
1545
|
+
| "Int8Array"
|
|
1528
1546
|
| "Uint8Array"
|
|
1547
|
+
| "Uint8ClampedArray"
|
|
1548
|
+
| "Int16Array"
|
|
1549
|
+
| "Uint16Array"
|
|
1550
|
+
| "Int32Array"
|
|
1551
|
+
| "Uint32Array"
|
|
1529
1552
|
| "AudioContext"
|
|
1530
1553
|
| "RegExp"
|
|
1531
1554
|
| "setTimeout"
|
|
@@ -1804,6 +1827,12 @@ fn check_unresolved_stmt(
|
|
|
1804
1827
|
}
|
|
1805
1828
|
scopes.pop();
|
|
1806
1829
|
}
|
|
1830
|
+
// Transparent group (comma-declarators): same scope, no push/pop.
|
|
1831
|
+
Statement::Multi { statements, .. } => {
|
|
1832
|
+
for s in statements {
|
|
1833
|
+
check_unresolved_stmt(s, scopes, out);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1807
1836
|
Statement::FunDecl {
|
|
1808
1837
|
name,
|
|
1809
1838
|
name_span,
|
|
@@ -1965,25 +1994,23 @@ fn enumerate_pattern_bindings(
|
|
|
1965
1994
|
) {
|
|
1966
1995
|
match pattern {
|
|
1967
1996
|
DestructPattern::Array(elements) => {
|
|
1968
|
-
for el in elements {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
enumerate_pattern_bindings(inner, kind, exported, out);
|
|
1979
|
-
}
|
|
1980
|
-
DestructElement::Rest(n, sp) => out.push(BindingSite {
|
|
1981
|
-
name: n.clone(),
|
|
1982
|
-
span: *sp,
|
|
1983
|
-
kind,
|
|
1984
|
-
exported,
|
|
1985
|
-
}),
|
|
1997
|
+
for el in elements.iter().flatten() {
|
|
1998
|
+
match el {
|
|
1999
|
+
DestructElement::Ident(n, sp) => out.push(BindingSite {
|
|
2000
|
+
name: n.clone(),
|
|
2001
|
+
span: *sp,
|
|
2002
|
+
kind,
|
|
2003
|
+
exported,
|
|
2004
|
+
}),
|
|
2005
|
+
DestructElement::Pattern(inner) => {
|
|
2006
|
+
enumerate_pattern_bindings(inner, kind, exported, out);
|
|
1986
2007
|
}
|
|
2008
|
+
DestructElement::Rest(n, sp) => out.push(BindingSite {
|
|
2009
|
+
name: n.clone(),
|
|
2010
|
+
span: *sp,
|
|
2011
|
+
kind,
|
|
2012
|
+
exported,
|
|
2013
|
+
}),
|
|
1987
2014
|
}
|
|
1988
2015
|
}
|
|
1989
2016
|
}
|
|
@@ -2300,6 +2327,11 @@ fn enumerate_stmt(stmt: &Statement, exported: bool, out: &mut Vec<BindingSite>)
|
|
|
2300
2327
|
enumerate_stmt(s, exported, out);
|
|
2301
2328
|
}
|
|
2302
2329
|
}
|
|
2330
|
+
Statement::Multi { statements, .. } => {
|
|
2331
|
+
for s in statements {
|
|
2332
|
+
enumerate_stmt(s, exported, out);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2303
2335
|
Statement::FunDecl {
|
|
2304
2336
|
name,
|
|
2305
2337
|
name_span,
|
|
@@ -2617,15 +2649,13 @@ fn param_layer_names(params: &[FunParam], rest_param: &Option<TypedParam>) -> Ve
|
|
|
2617
2649
|
fn collect_pattern_binding_names(pattern: &DestructPattern, out: &mut Vec<Arc<str>>) {
|
|
2618
2650
|
match pattern {
|
|
2619
2651
|
DestructPattern::Array(elements) => {
|
|
2620
|
-
for el in elements {
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
collect_pattern_binding_names(inner, out)
|
|
2626
|
-
}
|
|
2627
|
-
DestructElement::Rest(n, _) => out.push(n.clone()),
|
|
2652
|
+
for el in elements.iter().flatten() {
|
|
2653
|
+
match el {
|
|
2654
|
+
DestructElement::Ident(n, _) => out.push(n.clone()),
|
|
2655
|
+
DestructElement::Pattern(inner) => {
|
|
2656
|
+
collect_pattern_binding_names(inner, out)
|
|
2628
2657
|
}
|
|
2658
|
+
DestructElement::Rest(n, _) => out.push(n.clone()),
|
|
2629
2659
|
}
|
|
2630
2660
|
}
|
|
2631
2661
|
}
|
|
@@ -2912,6 +2942,11 @@ fn walk_stmt_completion(
|
|
|
2912
2942
|
walk_stmt_completion(s, source, lsp_line, lsp_char, stack, best);
|
|
2913
2943
|
}
|
|
2914
2944
|
}
|
|
2945
|
+
Statement::Multi { statements, .. } => {
|
|
2946
|
+
for s in statements {
|
|
2947
|
+
walk_stmt_completion(s, source, lsp_line, lsp_char, stack, best);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2915
2950
|
Statement::FunDecl {
|
|
2916
2951
|
params,
|
|
2917
2952
|
rest_param,
|
|
@@ -3181,6 +3216,11 @@ fn refs_stmt(
|
|
|
3181
3216
|
refs_stmt(s, program, source, name, def_span, out);
|
|
3182
3217
|
}
|
|
3183
3218
|
}
|
|
3219
|
+
Statement::Multi { statements, .. } => {
|
|
3220
|
+
for s in statements {
|
|
3221
|
+
refs_stmt(s, program, source, name, def_span, out);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3184
3224
|
Statement::FunDecl { params, body, .. } => {
|
|
3185
3225
|
for p in params {
|
|
3186
3226
|
if let FunParam::Simple(tp) = p {
|
|
@@ -56,6 +56,8 @@ http-hyper = [
|
|
|
56
56
|
]
|
|
57
57
|
fs = []
|
|
58
58
|
process = []
|
|
59
|
+
# Interactive terminal I/O (issue #101): raw mode, key/resize events, size, alt screen.
|
|
60
|
+
tty = ["dep:crossterm"]
|
|
59
61
|
regex = ["tishlang_core/regex"]
|
|
60
62
|
# WebSocket (JS-like API). Modular: import { WebSocket, Server } from 'tish:ws'.
|
|
61
63
|
# Requires `send-values` because the WS server keeps an `Arc<dyn Fn + Send +
|
|
@@ -84,6 +86,8 @@ tiny_http = { version = "0.12.0", optional = true }
|
|
|
84
86
|
socket2 = { version = "0.5", features = ["all"], optional = true }
|
|
85
87
|
libc = { version = "0.2", optional = true }
|
|
86
88
|
arc-swap = { version = "1", optional = true }
|
|
89
|
+
# Interactive terminal I/O (feature = tty): cross-platform raw mode, key/resize events, size.
|
|
90
|
+
crossterm = { version = "0.28", optional = true }
|
|
87
91
|
# hyper backend (feature = http-hyper)
|
|
88
92
|
hyper = { version = "1", default-features = false, features = ["server", "http1", "http2"], optional = true }
|
|
89
93
|
hyper-util = { version = "0.1", features = ["server", "tokio"], optional = true }
|
|
@@ -226,7 +226,27 @@ impl RequestPrimitive {
|
|
|
226
226
|
&& req.body_length().map(|n| n > 0).unwrap_or(true);
|
|
227
227
|
let mut body = String::new();
|
|
228
228
|
if has_body {
|
|
229
|
-
|
|
229
|
+
// Bounded read: cap at max_request_body so a huge advertised/chunked body can't
|
|
230
|
+
// exhaust memory. (`take` is unavailable on `&mut dyn Read`; read in chunks via
|
|
231
|
+
// the object-safe `read`, then decode once to keep UTF-8 boundaries intact.)
|
|
232
|
+
let max = max_request_body();
|
|
233
|
+
let reader = req.as_reader();
|
|
234
|
+
let mut buf: Vec<u8> = Vec::new();
|
|
235
|
+
let mut chunk = [0u8; 8192];
|
|
236
|
+
while buf.len() < max {
|
|
237
|
+
match reader.read(&mut chunk) {
|
|
238
|
+
Ok(0) => break,
|
|
239
|
+
Ok(n) => {
|
|
240
|
+
let take = n.min(max - buf.len());
|
|
241
|
+
buf.extend_from_slice(&chunk[..take]);
|
|
242
|
+
if take < n {
|
|
243
|
+
break; // hit the cap mid-chunk
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
Err(_) => break,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
body = String::from_utf8_lossy(&buf).into_owned();
|
|
230
250
|
}
|
|
231
251
|
let headers = req
|
|
232
252
|
.headers()
|
|
@@ -355,7 +375,7 @@ impl ResponsePrimitive {
|
|
|
355
375
|
}
|
|
356
376
|
} else if let Some(b) = obj_ref.strings.get("body") {
|
|
357
377
|
match b {
|
|
358
|
-
Value::String(s) => ResponseBody::Text(Arc::
|
|
378
|
+
Value::String(s) => ResponseBody::Text(Arc::from(s.as_str())),
|
|
359
379
|
Value::Array(a) => {
|
|
360
380
|
let borrow = a.borrow();
|
|
361
381
|
if !borrow.is_empty()
|
|
@@ -418,7 +438,7 @@ impl ResponsePrimitive {
|
|
|
418
438
|
Value::String(s) => Self {
|
|
419
439
|
status: default_status,
|
|
420
440
|
headers: vec![],
|
|
421
|
-
body: ResponseBody::Text(Arc::
|
|
441
|
+
body: ResponseBody::Text(Arc::from(s.as_str())),
|
|
422
442
|
},
|
|
423
443
|
_ => Self {
|
|
424
444
|
status: default_status,
|
|
@@ -447,7 +467,10 @@ pub fn value_to_response(value: &Value) -> (u16, Vec<(String, String)>, String)
|
|
|
447
467
|
(r.status, r.headers, body)
|
|
448
468
|
}
|
|
449
469
|
|
|
450
|
-
|
|
470
|
+
/// `(status, headers, file path)` extracted from a response object's `file` key.
|
|
471
|
+
type FileResponse = (u16, Vec<(String, String)>, String);
|
|
472
|
+
|
|
473
|
+
fn extract_file_from_response(value: &Value) -> Option<FileResponse> {
|
|
451
474
|
let Value::Object(obj) = value else {
|
|
452
475
|
return None;
|
|
453
476
|
};
|
|
@@ -504,7 +527,7 @@ pub fn send_response(
|
|
|
504
527
|
mut headers: Vec<(String, String)>,
|
|
505
528
|
body: String,
|
|
506
529
|
) {
|
|
507
|
-
send_response_arc(request, status, headers
|
|
530
|
+
send_response_arc(request, status, std::mem::take(&mut headers), Arc::from(body));
|
|
508
531
|
}
|
|
509
532
|
|
|
510
533
|
pub fn send_response_arc(
|
|
@@ -525,13 +548,39 @@ pub fn send_response_arc(
|
|
|
525
548
|
None,
|
|
526
549
|
);
|
|
527
550
|
for (key, value) in headers {
|
|
528
|
-
if let
|
|
551
|
+
if let Some(header) = safe_response_header(key.as_bytes(), value.as_bytes()) {
|
|
529
552
|
response = response.with_header(header);
|
|
530
553
|
}
|
|
531
554
|
}
|
|
532
555
|
let _ = request.respond(response);
|
|
533
556
|
}
|
|
534
557
|
|
|
558
|
+
/// Max request body bytes read before truncating — bounds per-request memory so a huge
|
|
559
|
+
/// `Content-Length` / chunked stream can't OOM the worker. Override with `TISH_HTTP_MAX_BODY`
|
|
560
|
+
/// (bytes); default 16 MiB. (A future refinement: reply 413 instead of truncating.)
|
|
561
|
+
fn max_request_body() -> usize {
|
|
562
|
+
use std::sync::OnceLock;
|
|
563
|
+
static MAX: OnceLock<usize> = OnceLock::new();
|
|
564
|
+
*MAX.get_or_init(|| {
|
|
565
|
+
std::env::var("TISH_HTTP_MAX_BODY")
|
|
566
|
+
.ok()
|
|
567
|
+
.and_then(|v| v.parse().ok())
|
|
568
|
+
.unwrap_or(16 * 1024 * 1024)
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/// Build a response header, rejecting any key/value with control bytes (CR/LF/NUL).
|
|
573
|
+
/// Prevents response-splitting / header injection when a handler reflects untrusted input
|
|
574
|
+
/// into a header — `tiny_http::Header::from_bytes` only checks ASCII, and CR/LF are ASCII,
|
|
575
|
+
/// so it would otherwise accept them.
|
|
576
|
+
fn safe_response_header(key: &[u8], value: &[u8]) -> Option<tiny_http::Header> {
|
|
577
|
+
let bad = |b: &u8| matches!(*b, b'\r' | b'\n' | 0);
|
|
578
|
+
if key.iter().any(bad) || value.iter().any(bad) {
|
|
579
|
+
return None;
|
|
580
|
+
}
|
|
581
|
+
tiny_http::Header::from_bytes(key, value).ok()
|
|
582
|
+
}
|
|
583
|
+
|
|
535
584
|
struct ArcBytesReader {
|
|
536
585
|
bytes: Arc<[u8]>,
|
|
537
586
|
pos: usize,
|
|
@@ -573,7 +622,7 @@ pub fn send_response_bytes(
|
|
|
573
622
|
None,
|
|
574
623
|
);
|
|
575
624
|
for (key, value) in headers {
|
|
576
|
-
if let
|
|
625
|
+
if let Some(header) = safe_response_header(key.as_bytes(), value.as_bytes()) {
|
|
577
626
|
response = response.with_header(header);
|
|
578
627
|
}
|
|
579
628
|
}
|
|
@@ -601,7 +650,7 @@ fn send_file_response(
|
|
|
601
650
|
let status_code = tiny_http::StatusCode(status);
|
|
602
651
|
let mut response = tiny_http::Response::from_file(file).with_status_code(status_code);
|
|
603
652
|
for (key, value) in headers {
|
|
604
|
-
if let
|
|
653
|
+
if let Some(header) = safe_response_header(key.as_bytes(), value.as_bytes()) {
|
|
605
654
|
response = response.with_header(header);
|
|
606
655
|
}
|
|
607
656
|
}
|
|
@@ -791,7 +840,7 @@ pub fn serve<F>(args: &[Value], handler: F) -> Value
|
|
|
791
840
|
where
|
|
792
841
|
F: Fn(&[Value]) -> Value + Send + Sync + 'static,
|
|
793
842
|
{
|
|
794
|
-
serve_impl_with_factory(args, None, Some(
|
|
843
|
+
serve_impl_with_factory(args, None, Some(tishlang_core::native_fn(handler)))
|
|
795
844
|
}
|
|
796
845
|
|
|
797
846
|
/// `serve(port, { onWorker, workers? })` — each accept thread calls the
|
|
@@ -804,12 +853,12 @@ where
|
|
|
804
853
|
#[cfg(feature = "send-values")]
|
|
805
854
|
pub fn serve_per_worker<FF>(args: &[Value], factory: FF) -> Value
|
|
806
855
|
where
|
|
807
|
-
FF: Fn(usize) ->
|
|
856
|
+
FF: Fn(usize) -> tishlang_core::NativeFn + Send + Sync + 'static,
|
|
808
857
|
{
|
|
809
858
|
serve_impl_with_factory(
|
|
810
859
|
args,
|
|
811
860
|
Some(Arc::new(factory)
|
|
812
|
-
as Arc<dyn Fn(usize) ->
|
|
861
|
+
as Arc<dyn Fn(usize) -> tishlang_core::NativeFn + Send + Sync>),
|
|
813
862
|
None,
|
|
814
863
|
)
|
|
815
864
|
}
|
|
@@ -829,9 +878,9 @@ where
|
|
|
829
878
|
fn serve_impl_with_factory(
|
|
830
879
|
args: &[Value],
|
|
831
880
|
factory: Option<
|
|
832
|
-
Arc<dyn Fn(usize) ->
|
|
881
|
+
Arc<dyn Fn(usize) -> tishlang_core::NativeFn + Send + Sync>,
|
|
833
882
|
>,
|
|
834
|
-
shared_handler: Option<
|
|
883
|
+
shared_handler: Option<tishlang_core::NativeFn>,
|
|
835
884
|
) -> Value {
|
|
836
885
|
debug_assert!(factory.is_some() ^ shared_handler.is_some());
|
|
837
886
|
let port = match args.first() {
|
|
@@ -958,7 +1007,7 @@ fn serve_impl_with_factory(
|
|
|
958
1007
|
let stop = Arc::clone(&stop);
|
|
959
1008
|
let processed = Arc::clone(&processed);
|
|
960
1009
|
let max = max_requests;
|
|
961
|
-
let worker_handler:
|
|
1010
|
+
let worker_handler: tishlang_core::NativeFn =
|
|
962
1011
|
if let Some(f) = &factory {
|
|
963
1012
|
f(global_worker_base + idx)
|
|
964
1013
|
} else {
|
|
@@ -1063,7 +1112,7 @@ where
|
|
|
1063
1112
|
let mut count = 0usize;
|
|
1064
1113
|
while let Ok((req_prim, resp_tx)) = rx.recv() {
|
|
1065
1114
|
let req_value = req_prim.into_value();
|
|
1066
|
-
let response_value = handler(&[req_value]);
|
|
1115
|
+
let response_value = handler.call(&[req_value]);
|
|
1067
1116
|
let resp_prim = ResponsePrimitive::from_value(&response_value);
|
|
1068
1117
|
let _ = resp_tx.send(resp_prim);
|
|
1069
1118
|
|
|
@@ -1092,7 +1141,7 @@ where
|
|
|
1092
1141
|
#[cfg(feature = "send-values")]
|
|
1093
1142
|
fn worker_loop_direct(
|
|
1094
1143
|
listener: std::net::TcpListener,
|
|
1095
|
-
handler:
|
|
1144
|
+
handler: tishlang_core::NativeFn,
|
|
1096
1145
|
stop: Arc<AtomicBool>,
|
|
1097
1146
|
processed: Arc<AtomicUsize>,
|
|
1098
1147
|
max_requests: Option<usize>,
|
|
@@ -1117,7 +1166,7 @@ fn worker_loop_direct(
|
|
|
1117
1166
|
} else {
|
|
1118
1167
|
let req_prim = RequestPrimitive::from_tiny_http(&mut request);
|
|
1119
1168
|
let req_value = req_prim.into_value();
|
|
1120
|
-
let response_value = handler(&[req_value]);
|
|
1169
|
+
let response_value = handler.call(&[req_value]);
|
|
1121
1170
|
let resp_prim = ResponsePrimitive::from_value(&response_value);
|
|
1122
1171
|
respond_from_primitive(request, resp_prim);
|
|
1123
1172
|
}
|
|
@@ -35,6 +35,7 @@ impl TishPromise for FetchResponsePromise {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
struct FetchAllResponsesPromise {
|
|
38
|
+
#[allow(clippy::type_complexity)]
|
|
38
39
|
rx: Mutex<
|
|
39
40
|
Option<
|
|
40
41
|
tokio::sync::oneshot::Receiver<Result<Vec<Result<reqwest::Response, String>>, String>>,
|
|
@@ -158,6 +159,7 @@ impl HttpBody {
|
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
#[allow(clippy::type_complexity)]
|
|
161
163
|
fn take_stream(
|
|
162
164
|
&self,
|
|
163
165
|
) -> Result<Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>, String> {
|
|
@@ -229,7 +231,7 @@ impl TishOpaque for HttpReadableStream {
|
|
|
229
231
|
return None;
|
|
230
232
|
}
|
|
231
233
|
let body = Arc::clone(&self.body);
|
|
232
|
-
Some(
|
|
234
|
+
Some(tishlang_core::native_fn(move |_args: &[Value]| match body.take_stream() {
|
|
233
235
|
Ok(stream) => {
|
|
234
236
|
let inner = Arc::new(tokio::sync::Mutex::new(StreamSlot { stream }));
|
|
235
237
|
Value::Opaque(Arc::new(HttpStreamReader {
|
|
@@ -270,7 +272,7 @@ impl TishOpaque for HttpStreamReader {
|
|
|
270
272
|
}
|
|
271
273
|
let inner = Arc::clone(&self.inner);
|
|
272
274
|
let body = Arc::clone(&self.body);
|
|
273
|
-
Some(
|
|
275
|
+
Some(tishlang_core::native_fn(move |_args: &[Value]| {
|
|
274
276
|
let inner = Arc::clone(&inner);
|
|
275
277
|
let body = Arc::clone(&body);
|
|
276
278
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
@@ -313,13 +315,13 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
313
315
|
let ok = response.status().is_success();
|
|
314
316
|
let headers_val = headers_to_value(response.headers());
|
|
315
317
|
let body_holder = Arc::new(HttpBody::new(response));
|
|
316
|
-
let stream =
|
|
318
|
+
let stream = HttpReadableStream {
|
|
317
319
|
body: Arc::clone(&body_holder),
|
|
318
|
-
}
|
|
319
|
-
let body_stream_val = Value::Opaque(stream);
|
|
320
|
+
};
|
|
321
|
+
let body_stream_val = Value::Opaque(Arc::new(stream));
|
|
320
322
|
let bh_text = Arc::clone(&body_holder);
|
|
321
323
|
let bh_json = Arc::clone(&body_holder);
|
|
322
|
-
let text_fn: NativeFn =
|
|
324
|
+
let text_fn: NativeFn = tishlang_core::native_fn(move |_args: &[Value]| {
|
|
323
325
|
let bh = Arc::clone(&bh_text);
|
|
324
326
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
325
327
|
crate::http::RUNTIME.with(|rt| {
|
|
@@ -330,7 +332,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
330
332
|
});
|
|
331
333
|
crate::promise_io::string_result_promise(rx)
|
|
332
334
|
});
|
|
333
|
-
let json_fn: NativeFn =
|
|
335
|
+
let json_fn: NativeFn = tishlang_core::native_fn(move |_args: &[Value]| {
|
|
334
336
|
let bh = Arc::clone(&bh_json);
|
|
335
337
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
336
338
|
crate::http::RUNTIME.with(|rt| {
|
|
@@ -353,13 +355,31 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
353
355
|
Value::object(obj)
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
/// Shared, hardened outbound HTTP client: request + connect timeouts (a no-timeout fetch is
|
|
359
|
+
/// an outbound resource-pinning / slowloris vector) and a bounded redirect chain. Cached so
|
|
360
|
+
/// the connection pool is reused across requests.
|
|
361
|
+
/// NOTE: this does NOT yet block internal/metadata IPs — full SSRF defense (deny loopback /
|
|
362
|
+
/// link-local / RFC1918 after DNS resolution) is a follow-up that needs a policy decision.
|
|
363
|
+
fn fetch_client() -> &'static reqwest::Client {
|
|
364
|
+
use std::sync::OnceLock;
|
|
365
|
+
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
|
366
|
+
CLIENT.get_or_init(|| {
|
|
367
|
+
reqwest::Client::builder()
|
|
368
|
+
.timeout(std::time::Duration::from_secs(30))
|
|
369
|
+
.connect_timeout(std::time::Duration::from_secs(10))
|
|
370
|
+
.redirect(reqwest::redirect::Policy::limited(5))
|
|
371
|
+
.build()
|
|
372
|
+
.unwrap_or_else(|_| reqwest::Client::new())
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
356
376
|
async fn send_request_parts(
|
|
357
377
|
url: String,
|
|
358
378
|
method: String,
|
|
359
379
|
headers: Vec<(String, String)>,
|
|
360
380
|
body: Option<String>,
|
|
361
381
|
) -> Result<reqwest::Response, String> {
|
|
362
|
-
let client =
|
|
382
|
+
let client = fetch_client();
|
|
363
383
|
let mut req = match method.as_str() {
|
|
364
384
|
"GET" => client.get(&url),
|
|
365
385
|
"POST" => client.post(&url),
|
|
@@ -416,6 +436,7 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
416
436
|
}));
|
|
417
437
|
}
|
|
418
438
|
};
|
|
439
|
+
#[allow(clippy::type_complexity)]
|
|
419
440
|
let mut parts: Vec<(String, String, Vec<(String, String)>, Option<String>)> = Vec::new();
|
|
420
441
|
for req in requests {
|
|
421
442
|
let (url, opt) = match &req {
|
|
@@ -186,6 +186,8 @@ where
|
|
|
186
186
|
let mut count = 0usize;
|
|
187
187
|
while let Ok((req_prim, resp_tx)) = rx.recv() {
|
|
188
188
|
let req_value = req_prim.into_value_pub();
|
|
189
|
+
// `handler` is the generic `F: Fn(&[Value]) -> Value` — call it directly. (`.call(..)` here
|
|
190
|
+
// would bind the *unstable* `Fn::call` trait method, which takes a tuple, not `&[Value]`.)
|
|
189
191
|
let response_value = handler(&[req_value]);
|
|
190
192
|
let resp_prim = ResponsePrimitive::from_value_pub(&response_value);
|
|
191
193
|
let _ = resp_tx.send(resp_prim);
|
|
@@ -259,6 +261,12 @@ fn worker_thread(
|
|
|
259
261
|
});
|
|
260
262
|
if let Err(e) = http1::Builder::new()
|
|
261
263
|
.keep_alive(true)
|
|
264
|
+
// A `Timer` MUST be installed whenever a timeout is set, or hyper 1.x panics
|
|
265
|
+
// ("header_read_timeout set, but no timer set") on the first connection.
|
|
266
|
+
.timer(hyper_util::rt::TokioTimer::new())
|
|
267
|
+
// Slowloris guard: drop a connection that dribbles its request
|
|
268
|
+
// headers slower than this instead of pinning the worker task.
|
|
269
|
+
.header_read_timeout(std::time::Duration::from_secs(30))
|
|
262
270
|
.serve_connection(io, svc)
|
|
263
271
|
.await
|
|
264
272
|
{
|
|
@@ -338,13 +346,28 @@ async fn extract_request(
|
|
|
338
346
|
.iter()
|
|
339
347
|
.map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
|
|
340
348
|
.collect();
|
|
341
|
-
|
|
349
|
+
// Cap the collected body so a huge/chunked request can't exhaust memory.
|
|
350
|
+
let limited = http_body_util::Limited::new(req.into_body(), hyper_max_body());
|
|
351
|
+
let body_bytes = match http_body_util::BodyExt::collect(limited).await {
|
|
342
352
|
Ok(c) => c.to_bytes().to_vec(),
|
|
343
|
-
Err(_) => Vec::new(),
|
|
353
|
+
Err(_) => Vec::new(), // includes "length limit exceeded"
|
|
344
354
|
};
|
|
345
355
|
(method, url, path, query, headers, body_bytes)
|
|
346
356
|
}
|
|
347
357
|
|
|
358
|
+
/// Max request body bytes for the hyper backend (mirrors the tiny_http cap).
|
|
359
|
+
/// Override with `TISH_HTTP_MAX_BODY` (bytes); default 16 MiB.
|
|
360
|
+
fn hyper_max_body() -> usize {
|
|
361
|
+
use std::sync::OnceLock;
|
|
362
|
+
static MAX: OnceLock<usize> = OnceLock::new();
|
|
363
|
+
*MAX.get_or_init(|| {
|
|
364
|
+
std::env::var("TISH_HTTP_MAX_BODY")
|
|
365
|
+
.ok()
|
|
366
|
+
.and_then(|v| v.parse().ok())
|
|
367
|
+
.unwrap_or(16 * 1024 * 1024)
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
fn primitive_to_hyper(resp: ResponsePrimitive) -> Response<Full<Bytes>> {
|
|
349
372
|
let (body_bytes, default_ct): (Bytes, Option<&str>) = match resp.body {
|
|
350
373
|
ResponseBody::Text(s) => (Bytes::copy_from_slice(s.as_bytes()), Some("text/plain")),
|