@tishlang/tish-lsp 3.0.0 → 3.2.2
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-lsp +0 -0
- package/crates/tish/src/cli_help.rs +4 -0
- package/crates/tish/src/main.rs +13 -2
- package/crates/tish/tests/rust_lib_crate.rs +140 -0
- package/crates/tish/tests/scope_merge.rs +31 -0
- package/crates/tish_build_utils/src/lib.rs +131 -6
- package/crates/tish_compile/src/codegen.rs +517 -19
- package/crates/tish_compile/src/lib.rs +14 -1
- package/crates/tish_compile/src/resolve.rs +12 -2
- package/crates/tish_compile/tests/regr_andor_rhs_move.rs +85 -0
- package/crates/tish_compile/tests/regr_rust_lib_emit.rs +150 -0
- package/crates/tish_core/src/shape.rs +28 -3
- package/crates/tish_fmt/src/lib.rs +54 -1
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +131 -0
- package/crates/tish_native/src/config.rs +14 -0
- package/crates/tish_runtime/Cargo.toml +20 -1
- package/crates/tish_runtime/src/crypto.rs +127 -0
- package/crates/tish_runtime/src/encoding.rs +157 -0
- package/crates/tish_runtime/src/fs_ext.rs +27 -0
- package/crates/tish_runtime/src/lib.rs +32 -0
- package/crates/tish_runtime/src/net.rs +482 -0
- package/crates/tish_runtime/src/process_spawn.rs +508 -0
- package/crates/tish_runtime/src/ws.rs +206 -23
- package/crates/tish_runtime/src/zip.rs +196 -0
- package/crates/tish_runtime_gba/src/lib.rs +17 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
//! Cryptographic hashing + secure random for Tish (`tish:crypto`), behind the `crypto` feature.
|
|
2
|
+
//!
|
|
3
|
+
//! import { sha256, sha256Hex, randomBytes } from 'tish:crypto'
|
|
4
|
+
//!
|
|
5
|
+
//! - `sha256(bytesOrString) -> number[]` (the 32-byte digest as a byte array)
|
|
6
|
+
//! - `sha256Hex(bytesOrString) -> string` (64 lowercase hex chars)
|
|
7
|
+
//! - `randomBytes(n) -> number[]` (n cryptographically-secure random bytes; n clamped to 1 MiB)
|
|
8
|
+
//!
|
|
9
|
+
//! Bytes are the `tish:fs` / `tish:encoding` representation (Numbers 0-255). A string argument is
|
|
10
|
+
//! hashed as its UTF-8 bytes. For PKCE: `base64UrlEncode(sha256(verifier))` (see `tish:encoding`).
|
|
11
|
+
|
|
12
|
+
use sha2::{Digest, Sha256};
|
|
13
|
+
use tishlang_core::{Value, VmRef};
|
|
14
|
+
|
|
15
|
+
/// Coerce arg 0 to bytes: a byte array (Numbers 0-255) or a string's UTF-8 bytes.
|
|
16
|
+
fn arg_bytes(args: &[Value]) -> Option<Vec<u8>> {
|
|
17
|
+
match args.first() {
|
|
18
|
+
Some(Value::Array(a)) => Some(
|
|
19
|
+
a.borrow()
|
|
20
|
+
.iter()
|
|
21
|
+
.map(|v| if let Value::Number(n) = v { *n as u8 } else { 0 })
|
|
22
|
+
.collect(),
|
|
23
|
+
),
|
|
24
|
+
Some(Value::String(s)) => Some(s.as_bytes().to_vec()),
|
|
25
|
+
_ => None,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn bytes_to_value(bytes: &[u8]) -> Value {
|
|
30
|
+
Value::Array(VmRef::new(
|
|
31
|
+
bytes.iter().map(|b| Value::Number(*b as f64)).collect(),
|
|
32
|
+
))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn digest_of(args: &[Value]) -> Option<[u8; 32]> {
|
|
36
|
+
let b = arg_bytes(args)?;
|
|
37
|
+
let mut h = Sha256::new();
|
|
38
|
+
h.update(&b);
|
|
39
|
+
Some(h.finalize().into())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn sha256(args: &[Value]) -> Value {
|
|
43
|
+
match digest_of(args) {
|
|
44
|
+
Some(d) => bytes_to_value(&d),
|
|
45
|
+
None => Value::Null,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn sha256_hex(args: &[Value]) -> Value {
|
|
50
|
+
match digest_of(args) {
|
|
51
|
+
Some(d) => {
|
|
52
|
+
let mut s = String::with_capacity(64);
|
|
53
|
+
for byte in d.iter() {
|
|
54
|
+
s.push_str(&format!("{byte:02x}"));
|
|
55
|
+
}
|
|
56
|
+
Value::String(s.into())
|
|
57
|
+
}
|
|
58
|
+
None => Value::Null,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn random_bytes(args: &[Value]) -> Value {
|
|
63
|
+
let n = match args.first() {
|
|
64
|
+
Some(Value::Number(x)) if x.is_finite() && *x >= 0.0 => (*x as usize).min(1_048_576),
|
|
65
|
+
_ => return Value::Null,
|
|
66
|
+
};
|
|
67
|
+
// rand::random draws from the thread CSPRNG (ChaCha, OS-seeded) — same source fs_ext uses.
|
|
68
|
+
let bytes: Vec<u8> = (0..n).map(|_| rand::random::<u8>()).collect();
|
|
69
|
+
bytes_to_value(&bytes)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[cfg(test)]
|
|
73
|
+
mod tests {
|
|
74
|
+
use super::*;
|
|
75
|
+
|
|
76
|
+
fn to_bytes(v: Value) -> Vec<u8> {
|
|
77
|
+
let Value::Array(a) = v else { panic!("not an array") };
|
|
78
|
+
let g = a.borrow();
|
|
79
|
+
let out: Vec<u8> = g
|
|
80
|
+
.iter()
|
|
81
|
+
.map(|x| if let Value::Number(n) = x { *n as u8 } else { 0 })
|
|
82
|
+
.collect();
|
|
83
|
+
out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn sha256_hex_known_vector() {
|
|
88
|
+
// The canonical NIST vector: SHA-256("abc").
|
|
89
|
+
let Value::String(s) = sha256_hex(&[Value::String("abc".into())]) else { panic!() };
|
|
90
|
+
assert_eq!(
|
|
91
|
+
s.to_string(),
|
|
92
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn sha256_returns_32_bytes_matching_hex() {
|
|
98
|
+
let bytes = to_bytes(sha256(&[Value::String("abc".into())]));
|
|
99
|
+
assert_eq!(bytes.len(), 32);
|
|
100
|
+
assert_eq!(bytes[0], 0xba);
|
|
101
|
+
assert_eq!(bytes[31], 0xad);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn sha256_accepts_byte_array() {
|
|
106
|
+
// "abc" as a byte array must hash identically to the string.
|
|
107
|
+
let arr = Value::Array(VmRef::new(vec![
|
|
108
|
+
Value::Number(97.0),
|
|
109
|
+
Value::Number(98.0),
|
|
110
|
+
Value::Number(99.0),
|
|
111
|
+
]));
|
|
112
|
+
let Value::String(s) = sha256_hex(&[arr]) else { panic!() };
|
|
113
|
+
assert_eq!(
|
|
114
|
+
s.to_string(),
|
|
115
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn random_bytes_length_and_variance() {
|
|
121
|
+
assert_eq!(to_bytes(random_bytes(&[Value::Number(0.0)])).len(), 0);
|
|
122
|
+
let a = to_bytes(random_bytes(&[Value::Number(32.0)]));
|
|
123
|
+
let b = to_bytes(random_bytes(&[Value::Number(32.0)]));
|
|
124
|
+
assert_eq!(a.len(), 32);
|
|
125
|
+
assert_ne!(a, b, "two 32-byte draws must (almost surely) differ");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
//! Base64 encoding for Tish (`tish:encoding`), behind the `encoding` feature.
|
|
2
|
+
//!
|
|
3
|
+
//! import { base64Encode, base64Decode, base64UrlEncode, base64UrlDecode } from 'tish:encoding'
|
|
4
|
+
//!
|
|
5
|
+
//! - `base64Encode(bytesOrString) -> string` (standard alphabet, padded)
|
|
6
|
+
//! - `base64Decode(string) -> number[] | null` (bytes 0-255; null on invalid input)
|
|
7
|
+
//! - `base64UrlEncode(bytesOrString) -> string` (URL-safe alphabet, NO padding — PKCE etc.)
|
|
8
|
+
//! - `base64UrlDecode(string) -> number[] | null`
|
|
9
|
+
//!
|
|
10
|
+
//! Bytes are the same representation `tish:fs` uses: an array of Numbers 0-255 (`readFileBytes`
|
|
11
|
+
//! returns this; `writeFile` accepts it). A string argument is encoded as its UTF-8 bytes, so
|
|
12
|
+
//! `base64Encode("hi")` works without a byte round-trip.
|
|
13
|
+
|
|
14
|
+
use base64::Engine;
|
|
15
|
+
use tishlang_core::{Value, VmRef};
|
|
16
|
+
|
|
17
|
+
/// Coerce arg 0 to bytes: a byte array (Numbers 0-255) or a string's UTF-8 bytes.
|
|
18
|
+
fn arg_bytes(args: &[Value]) -> Option<Vec<u8>> {
|
|
19
|
+
match args.first() {
|
|
20
|
+
Some(Value::Array(a)) => Some(
|
|
21
|
+
a.borrow()
|
|
22
|
+
.iter()
|
|
23
|
+
.map(|v| if let Value::Number(n) = v { *n as u8 } else { 0 })
|
|
24
|
+
.collect(),
|
|
25
|
+
),
|
|
26
|
+
Some(Value::String(s)) => Some(s.as_bytes().to_vec()),
|
|
27
|
+
_ => None,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn bytes_to_value(bytes: Vec<u8>) -> Value {
|
|
32
|
+
Value::Array(VmRef::new(
|
|
33
|
+
bytes.into_iter().map(|b| Value::Number(b as f64)).collect(),
|
|
34
|
+
))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn base64_encode(args: &[Value]) -> Value {
|
|
38
|
+
match arg_bytes(args) {
|
|
39
|
+
Some(b) => Value::String(base64::engine::general_purpose::STANDARD.encode(&b).into()),
|
|
40
|
+
None => Value::Null,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn base64_decode(args: &[Value]) -> Value {
|
|
45
|
+
let s = match args.first() {
|
|
46
|
+
Some(Value::String(s)) => s.to_string(),
|
|
47
|
+
_ => return Value::Null,
|
|
48
|
+
};
|
|
49
|
+
match base64::engine::general_purpose::STANDARD.decode(s.as_bytes()) {
|
|
50
|
+
Ok(b) => bytes_to_value(b),
|
|
51
|
+
Err(_) => Value::Null,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub fn base64_url_encode(args: &[Value]) -> Value {
|
|
56
|
+
match arg_bytes(args) {
|
|
57
|
+
Some(b) => Value::String(
|
|
58
|
+
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
59
|
+
.encode(&b)
|
|
60
|
+
.into(),
|
|
61
|
+
),
|
|
62
|
+
None => Value::Null,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn base64_url_decode(args: &[Value]) -> Value {
|
|
67
|
+
let s = match args.first() {
|
|
68
|
+
Some(Value::String(s)) => s.to_string(),
|
|
69
|
+
_ => return Value::Null,
|
|
70
|
+
};
|
|
71
|
+
match base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s.as_bytes()) {
|
|
72
|
+
Ok(b) => bytes_to_value(b),
|
|
73
|
+
Err(_) => Value::Null,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// `utf8Decode(byteArray) -> string` — decode bytes as UTF-8 (lossy: invalid sequences become the
|
|
78
|
+
/// replacement char). The counterpart to `readFileBytes` / a streaming `reader.read()` chunk, whose
|
|
79
|
+
/// `value` is a byte array. Split streamed bytes on a newline BEFORE decoding a line so a multi-byte
|
|
80
|
+
/// sequence is never cut mid-decode (a `\n`=0x0A byte never occurs inside a UTF-8 multi-byte char).
|
|
81
|
+
pub fn utf8_decode(args: &[Value]) -> Value {
|
|
82
|
+
match arg_bytes(args) {
|
|
83
|
+
Some(b) => Value::String(String::from_utf8_lossy(&b).into_owned().into()),
|
|
84
|
+
None => Value::Null,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// `utf8Encode(string) -> number[]` — the string's UTF-8 bytes as a byte array. (A byte-array
|
|
89
|
+
/// argument is passed through, so `utf8Encode(utf8Decode(x))` is a no-op round trip.)
|
|
90
|
+
pub fn utf8_encode(args: &[Value]) -> Value {
|
|
91
|
+
match arg_bytes(args) {
|
|
92
|
+
Some(b) => bytes_to_value(b),
|
|
93
|
+
None => Value::Null,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[cfg(test)]
|
|
98
|
+
mod tests {
|
|
99
|
+
use super::*;
|
|
100
|
+
|
|
101
|
+
fn to_bytes(v: Value) -> Vec<u8> {
|
|
102
|
+
let Value::Array(a) = v else { panic!("not an array") };
|
|
103
|
+
let g = a.borrow();
|
|
104
|
+
let out: Vec<u8> = g
|
|
105
|
+
.iter()
|
|
106
|
+
.map(|x| if let Value::Number(n) = x { *n as u8 } else { 0 })
|
|
107
|
+
.collect();
|
|
108
|
+
out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn encode_string_matches_known_vector() {
|
|
113
|
+
let enc = base64_encode(&[Value::String("hello world".into())]);
|
|
114
|
+
let Value::String(s) = &enc else { panic!() };
|
|
115
|
+
assert_eq!(s.to_string(), "aGVsbG8gd29ybGQ=");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn roundtrip_all_byte_values() {
|
|
120
|
+
let arr = Value::Array(VmRef::new(
|
|
121
|
+
(0u8..=255).map(|b| Value::Number(b as f64)).collect(),
|
|
122
|
+
));
|
|
123
|
+
let enc = base64_encode(&[arr]);
|
|
124
|
+
assert_eq!(to_bytes(base64_decode(&[enc])), (0u8..=255).collect::<Vec<u8>>());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn url_safe_has_no_pad_plus_or_slash() {
|
|
129
|
+
// 0xFB,0xFF,0xBF encodes to +/ in standard; url-safe must use -_ and drop padding.
|
|
130
|
+
let arr = Value::Array(VmRef::new(vec![
|
|
131
|
+
Value::Number(251.0),
|
|
132
|
+
Value::Number(255.0),
|
|
133
|
+
Value::Number(191.0),
|
|
134
|
+
]));
|
|
135
|
+
let Value::String(sv) = base64_url_encode(&[arr]) else { panic!() };
|
|
136
|
+
let s = sv.to_string();
|
|
137
|
+
assert!(!s.contains('=') && !s.contains('+') && !s.contains('/'), "got {s}");
|
|
138
|
+
// and it round-trips
|
|
139
|
+
assert_eq!(to_bytes(base64_url_decode(&[Value::String(sv)])), vec![251u8, 255, 191]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn decode_invalid_is_null() {
|
|
144
|
+
assert!(matches!(
|
|
145
|
+
base64_decode(&[Value::String("!!! not base64 !!!".into())]),
|
|
146
|
+
Value::Null
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn utf8_roundtrip_multibyte() {
|
|
152
|
+
// "héllo 🚀" exercises 2-byte (é) and 4-byte (🚀) sequences.
|
|
153
|
+
let bytes = utf8_encode(&[Value::String("héllo 🚀".into())]);
|
|
154
|
+
let Value::String(s) = utf8_decode(&[bytes]) else { panic!() };
|
|
155
|
+
assert_eq!(s.to_string(), "héllo 🚀");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -230,6 +230,32 @@ pub fn truncate_core(args: &[Value]) -> Result<Value, Value> {
|
|
|
230
230
|
.map_err(io_err)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/// `chmod(path, mode)` — set the file mode (a number, e.g. `0o600` = 384). Unix only; a no-op that
|
|
234
|
+
/// succeeds on other platforms. Needed to write a secrets file 0600.
|
|
235
|
+
pub fn chmod_core(args: &[Value]) -> Result<Value, Value> {
|
|
236
|
+
let mode = match args.get(1) {
|
|
237
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => *n as u32,
|
|
238
|
+
_ => {
|
|
239
|
+
return Err(io_err(std::io::Error::new(
|
|
240
|
+
std::io::ErrorKind::InvalidInput,
|
|
241
|
+
"chmod: mode must be a number",
|
|
242
|
+
)))
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
#[cfg(unix)]
|
|
246
|
+
{
|
|
247
|
+
use std::os::unix::fs::PermissionsExt;
|
|
248
|
+
std::fs::set_permissions(path_arg(args, 0), std::fs::Permissions::from_mode(mode))
|
|
249
|
+
.map(|_| Value::Null)
|
|
250
|
+
.map_err(io_err)
|
|
251
|
+
}
|
|
252
|
+
#[cfg(not(unix))]
|
|
253
|
+
{
|
|
254
|
+
let _ = mode;
|
|
255
|
+
Ok(Value::Null)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
233
259
|
/// `mkdtemp(prefix)` — create a uniquely-named temp dir and return its path. Node appends 6 RANDOM
|
|
234
260
|
/// characters; a timestamp suffix is both predictable (a temp-dir security smell) and collision-prone
|
|
235
261
|
/// under rapid calls. Use a random suffix and rely on `create_dir`'s exclusive semantics, retrying on
|
|
@@ -319,6 +345,7 @@ fs_method!(copy_file, copy_file_promise, copy_file_core);
|
|
|
319
345
|
fs_method!(realpath, realpath_promise, realpath_core);
|
|
320
346
|
fs_method!(readlink, readlink_promise, readlink_core);
|
|
321
347
|
fs_method!(truncate, truncate_promise, truncate_core);
|
|
348
|
+
fs_method!(chmod, chmod_promise, chmod_core);
|
|
322
349
|
fs_method!(mkdtemp, mkdtemp_promise, mkdtemp_core);
|
|
323
350
|
fs_method!(cp, cp_promise, cp_core);
|
|
324
351
|
|
|
@@ -1778,6 +1778,38 @@ pub mod pty;
|
|
|
1778
1778
|
#[cfg(feature = "pty")]
|
|
1779
1779
|
pub use pty::{pty_kill, pty_pid, pty_read, pty_resize, pty_spawn, pty_write};
|
|
1780
1780
|
|
|
1781
|
+
#[cfg(feature = "process")]
|
|
1782
|
+
pub mod process_spawn;
|
|
1783
|
+
#[cfg(feature = "process")]
|
|
1784
|
+
pub use process_spawn::{
|
|
1785
|
+
process_close_stdin, process_kill, process_pid, process_read_stderr, process_read_stdout,
|
|
1786
|
+
process_spawn, process_wait, process_write_stdin,
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
#[cfg(feature = "net")]
|
|
1790
|
+
pub mod net;
|
|
1791
|
+
#[cfg(feature = "net")]
|
|
1792
|
+
pub use net::{
|
|
1793
|
+
net_accept, net_close, net_connect, net_listen, net_probe, net_read, net_sleep, net_write,
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
#[cfg(feature = "encoding")]
|
|
1797
|
+
pub mod encoding;
|
|
1798
|
+
#[cfg(feature = "encoding")]
|
|
1799
|
+
pub use encoding::{
|
|
1800
|
+
base64_decode, base64_encode, base64_url_decode, base64_url_encode, utf8_decode, utf8_encode,
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
#[cfg(feature = "crypto")]
|
|
1804
|
+
pub mod crypto;
|
|
1805
|
+
#[cfg(feature = "crypto")]
|
|
1806
|
+
pub use crypto::{random_bytes, sha256, sha256_hex};
|
|
1807
|
+
|
|
1808
|
+
#[cfg(feature = "zip")]
|
|
1809
|
+
pub mod zip;
|
|
1810
|
+
#[cfg(feature = "zip")]
|
|
1811
|
+
pub use zip::{zip_entries, zip_extract};
|
|
1812
|
+
|
|
1781
1813
|
#[cfg(feature = "ws")]
|
|
1782
1814
|
pub use ws::{
|
|
1783
1815
|
web_socket_client, web_socket_server_accept, web_socket_server_construct,
|