@tishlang/tish-lint 2.36.2 → 2.37.0
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/bin/tish-lint +0 -0
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_runtime/Cargo.toml +1 -1
- package/crates/tish_runtime/src/http.rs +51 -0
- package/crates/tish_runtime/src/http_fetch.rs +50 -6
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
package/bin/tish-lint
CHANGED
|
Binary file
|
|
@@ -79,7 +79,7 @@ ws = [
|
|
|
79
79
|
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
80
80
|
tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
|
|
81
81
|
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "time", "sync"], optional = true }
|
|
82
|
-
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream"], optional = true }
|
|
82
|
+
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream", "multipart"], optional = true }
|
|
83
83
|
futures = { version = "0.3.32", optional = true }
|
|
84
84
|
bytes = { version = "1", optional = true }
|
|
85
85
|
tiny_http = { version = "0.12.0", optional = true }
|
|
@@ -111,6 +111,57 @@ pub(crate) fn extract_body(options: Option<&Value>) -> Option<String> {
|
|
|
111
111
|
})
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/// A single `multipart/form-data` part extracted from a `fetch` options object's `multipart` array.
|
|
115
|
+
pub(crate) enum MultipartPart {
|
|
116
|
+
/// A text field: `{ name, value }`.
|
|
117
|
+
Text { name: String, value: String },
|
|
118
|
+
/// A file field read from disk (binary-safe): `{ name, path, filename?, contentType? }`.
|
|
119
|
+
File {
|
|
120
|
+
name: String,
|
|
121
|
+
path: String,
|
|
122
|
+
filename: Option<String>,
|
|
123
|
+
content_type: Option<String>,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Extract the `multipart` option — an array of part descriptors — from a `fetch` options object.
|
|
128
|
+
/// Each part is `{ name, value }` (text) or `{ name, path, filename?, contentType? }` (file, read as
|
|
129
|
+
/// bytes so binary uploads work). Returns `None` when there is no usable `multipart` array, so callers
|
|
130
|
+
/// fall back to `body`. A part missing `name`, or with neither `path` nor `value`, is skipped.
|
|
131
|
+
pub(crate) fn extract_multipart(options: Option<&Value>) -> Option<Vec<MultipartPart>> {
|
|
132
|
+
let field = options.and_then(|v| match v {
|
|
133
|
+
Value::Object(obj) => obj.borrow().strings.get("multipart").cloned(),
|
|
134
|
+
_ => None,
|
|
135
|
+
})?;
|
|
136
|
+
let items = match field {
|
|
137
|
+
Value::Array(a) => a.borrow().clone(),
|
|
138
|
+
_ => return None,
|
|
139
|
+
};
|
|
140
|
+
let mut parts = Vec::with_capacity(items.len());
|
|
141
|
+
for item in items {
|
|
142
|
+
let obj = match &item {
|
|
143
|
+
Value::Object(o) => o.borrow(),
|
|
144
|
+
_ => continue,
|
|
145
|
+
};
|
|
146
|
+
let name = match obj.strings.get("name") {
|
|
147
|
+
Some(v) => v.to_display_string(),
|
|
148
|
+
None => continue,
|
|
149
|
+
};
|
|
150
|
+
let filename = obj.strings.get("filename").map(|v| v.to_display_string());
|
|
151
|
+
let content_type = obj.strings.get("contentType").map(|v| v.to_display_string());
|
|
152
|
+
if let Some(path) = obj.strings.get("path").map(|v| v.to_display_string()) {
|
|
153
|
+
parts.push(MultipartPart::File { name, path, filename, content_type });
|
|
154
|
+
} else if let Some(value) = obj.strings.get("value").map(|v| v.to_display_string()) {
|
|
155
|
+
parts.push(MultipartPart::Text { name, value });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if parts.is_empty() {
|
|
159
|
+
None
|
|
160
|
+
} else {
|
|
161
|
+
Some(parts)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
114
165
|
pub(crate) fn build_error_response(error: &str) -> Value {
|
|
115
166
|
let mut obj: ObjectMap = ObjectMap::with_capacity(2);
|
|
116
167
|
obj.insert(Arc::from("error"), Value::String(error.into()));
|
|
@@ -10,7 +10,10 @@ use futures::Stream;
|
|
|
10
10
|
use futures::StreamExt;
|
|
11
11
|
use tishlang_core::{NativeFn, ObjectMap, TishOpaque, TishPromise, Value};
|
|
12
12
|
|
|
13
|
-
use crate::http::{
|
|
13
|
+
use crate::http::{
|
|
14
|
+
build_error_response, extract_body, extract_headers, extract_method, extract_multipart,
|
|
15
|
+
MultipartPart,
|
|
16
|
+
};
|
|
14
17
|
|
|
15
18
|
// --- Promises (Send payloads only; Value built on awaiting thread) ---
|
|
16
19
|
|
|
@@ -479,6 +482,7 @@ async fn send_request_parts(
|
|
|
479
482
|
method: String,
|
|
480
483
|
headers: Vec<(String, String)>,
|
|
481
484
|
body: Option<String>,
|
|
485
|
+
multipart: Option<Vec<MultipartPart>>,
|
|
482
486
|
) -> Result<reqwest::Response, String> {
|
|
483
487
|
if fetch_block_private() {
|
|
484
488
|
ssrf_preflight(&url).await?;
|
|
@@ -496,7 +500,39 @@ async fn send_request_parts(
|
|
|
496
500
|
for (key, value) in headers {
|
|
497
501
|
req = req.header(key, value);
|
|
498
502
|
}
|
|
499
|
-
|
|
503
|
+
// A `multipart` option takes precedence over `body` — it builds a multipart/form-data request
|
|
504
|
+
// (reqwest sets the Content-Type + boundary). File parts are read as bytes, so binary uploads work.
|
|
505
|
+
if let Some(parts) = multipart {
|
|
506
|
+
let mut form = reqwest::multipart::Form::new();
|
|
507
|
+
for part in parts {
|
|
508
|
+
match part {
|
|
509
|
+
MultipartPart::Text { name, value } => {
|
|
510
|
+
form = form.text(name, value);
|
|
511
|
+
}
|
|
512
|
+
MultipartPart::File {
|
|
513
|
+
name,
|
|
514
|
+
path,
|
|
515
|
+
filename,
|
|
516
|
+
content_type,
|
|
517
|
+
} => {
|
|
518
|
+
let bytes = std::fs::read(&path)
|
|
519
|
+
.map_err(|e| format!("multipart file '{path}': {e}"))?;
|
|
520
|
+
let fname = filename.unwrap_or_else(|| {
|
|
521
|
+
std::path::Path::new(&path)
|
|
522
|
+
.file_name()
|
|
523
|
+
.map(|s| s.to_string_lossy().into_owned())
|
|
524
|
+
.unwrap_or_else(|| name.clone())
|
|
525
|
+
});
|
|
526
|
+
let mut p = reqwest::multipart::Part::bytes(bytes).file_name(fname);
|
|
527
|
+
if let Some(ct) = content_type {
|
|
528
|
+
p = p.mime_str(&ct).map_err(|e| e.to_string())?;
|
|
529
|
+
}
|
|
530
|
+
form = form.part(name, p);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
req = req.multipart(form);
|
|
535
|
+
} else if let Some(body) = body {
|
|
500
536
|
req = req.body(body);
|
|
501
537
|
}
|
|
502
538
|
req.send().await.map_err(|e| e.to_string())
|
|
@@ -517,10 +553,11 @@ pub fn fetch_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
517
553
|
let method = extract_method(args.get(1));
|
|
518
554
|
let headers = extract_headers(args.get(1));
|
|
519
555
|
let body = extract_body(args.get(1));
|
|
556
|
+
let multipart = extract_multipart(args.get(1));
|
|
520
557
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
521
558
|
crate::http::RUNTIME.with(|rt| {
|
|
522
559
|
rt.spawn(async move {
|
|
523
|
-
let r = send_request_parts(url, method, headers, body).await;
|
|
560
|
+
let r = send_request_parts(url, method, headers, body, multipart).await;
|
|
524
561
|
let _ = tx.send(r);
|
|
525
562
|
});
|
|
526
563
|
});
|
|
@@ -541,7 +578,13 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
541
578
|
}
|
|
542
579
|
};
|
|
543
580
|
#[allow(clippy::type_complexity)]
|
|
544
|
-
let mut parts: Vec<(
|
|
581
|
+
let mut parts: Vec<(
|
|
582
|
+
String,
|
|
583
|
+
String,
|
|
584
|
+
Vec<(String, String)>,
|
|
585
|
+
Option<String>,
|
|
586
|
+
Option<Vec<MultipartPart>>,
|
|
587
|
+
)> = Vec::new();
|
|
545
588
|
for req in requests {
|
|
546
589
|
let (url, opt) = match &req {
|
|
547
590
|
Value::String(s) => (s.to_string(), None),
|
|
@@ -576,14 +619,15 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
576
619
|
let method = extract_method(opt.as_ref());
|
|
577
620
|
let headers = extract_headers(opt.as_ref());
|
|
578
621
|
let body = extract_body(opt.as_ref());
|
|
579
|
-
|
|
622
|
+
let multipart = extract_multipart(opt.as_ref());
|
|
623
|
+
parts.push((url, method, headers, body, multipart));
|
|
580
624
|
}
|
|
581
625
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
582
626
|
crate::http::RUNTIME.with(|rt| {
|
|
583
627
|
rt.spawn(async move {
|
|
584
628
|
let futs: Vec<_> = parts
|
|
585
629
|
.into_iter()
|
|
586
|
-
.map(|(url, m, h, b)| send_request_parts(url, m, h, b))
|
|
630
|
+
.map(|(url, m, h, b, mp)| send_request_parts(url, m, h, b, mp))
|
|
587
631
|
.collect();
|
|
588
632
|
let results = futures::future::join_all(futs).await;
|
|
589
633
|
let mapped: Vec<Result<reqwest::Response, String>> = results.into_iter().collect();
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|