Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
bc905bad
authored
Jun 27, 2026
by
DigHuang
Committed by
GitHub
Jun 27, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(ide-agent): add filesystem exec and path operations support (#7191)
parent
42b661ca
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
502 additions
and
565 deletions
+502
-565
projects/fastgpt-ide-agent/Cargo.lock
+44
-245
projects/fastgpt-ide-agent/Cargo.toml
+2
-2
projects/fastgpt-ide-agent/src/connection.rs
+30
-23
projects/fastgpt-ide-agent/src/fs.rs
+377
-262
projects/fastgpt-ide-agent/src/main.rs
+5
-1
projects/fastgpt-ide-agent/src/password.rs
+19
-8
projects/fastgpt-ide-agent/src/workspace.rs
+25
-24
No files found.
projects/fastgpt-ide-agent/Cargo.lock
View file @
bc905bad
...
...
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anyhow"
version = "1.0.10
2
"
version = "1.0.10
3
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c
"
checksum = "
2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3
"
[[package]]
name = "base64"
...
...
@@ -22,9 +22,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.1
1.1
"
version = "2.1
3.0
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3
"
checksum = "
b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8
"
[[package]]
name = "block-buffer"
...
...
@@ -37,9 +37,9 @@ dependencies = [
[[package]]
name = "bytes"
version = "1.1
1.1
"
version = "1.1
2.0
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc3
3"
checksum = "
8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb59
3"
[[package]]
name = "cfg-if"
...
...
@@ -95,12 +95,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -154,12 +148,6 @@ dependencies = [
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -226,43 +214,20 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.4.
2
"
version = "0.4.
3
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555
"
checksum = "
300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099
"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.4.
1
"
version = "1.4.
2
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0
"
checksum = "
6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425
"
dependencies = [
"bytes",
"itoa",
...
...
@@ -275,30 +240,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
name = "inotify"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533e68a5842e734946fe159fb03fc9bbbb254f590dd0d8ad321ae5ff7beca2c1"
dependencies = [
"bitflags 2.1
1.1
",
"bitflags 2.1
3.0
",
"inotify-sys",
"libc",
]
...
...
@@ -334,7 +281,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
dependencies = [
"bitflags 2.1
1.1
",
"bitflags 2.1
3.0
",
"libc",
]
...
...
@@ -345,12 +292,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -364,15 +305,15 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.3
0
"
version = "0.4.3
3
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5
"
checksum = "
0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad
"
[[package]]
name = "memchr"
version = "2.8.
1
"
version = "2.8.
2
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8
"
checksum = "
88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4
"
[[package]]
name = "mio"
...
...
@@ -392,7 +333,7 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.1
1.1
",
"bitflags 2.1
3.0
",
"cfg-if",
"cfg_aliases",
"libc",
...
...
@@ -404,7 +345,7 @@ version = "9.0.0-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b44b771d4dd781ef14c84078693e67495da6b47f609f72e8a4da8420a861240e"
dependencies = [
"bitflags 2.1
1.1
",
"bitflags 2.1
3.0
",
"inotify",
"kqueue",
"libc",
...
...
@@ -438,7 +379,7 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
dependencies = [
"bitflags 2.1
1.1
",
"bitflags 2.1
3.0
",
]
[[package]]
...
...
@@ -447,7 +388,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.1
1.1
",
"bitflags 2.1
3.0
",
]
[[package]]
...
...
@@ -512,16 +453,6 @@ dependencies = [
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -532,9 +463,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.4
5
"
version = "1.0.4
6
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924
"
checksum = "
dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368
"
dependencies = [
"proc-macro2",
]
...
...
@@ -592,7 +523,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.1
1.1
",
"bitflags 2.1
3.0
",
"errno",
"libc",
"linux-raw-sys",
...
...
@@ -609,12 +540,6 @@ dependencies = [
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -696,6 +621,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -713,9 +648,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.11
7
"
version = "2.0.11
8
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99
"
checksum = "
1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422
"
dependencies = [
"proc-macro2",
"quote",
...
...
@@ -729,7 +664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.
2
",
"getrandom 0.4.
3
",
"once_cell",
"rustix",
"windows-sys 0.61.2",
...
...
@@ -785,6 +720,7 @@ dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
...
...
@@ -842,12 +778,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -871,54 +801,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.
3+wasi-0.2.9
"
version = "1.0.
4+wasi-0.2.12
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6
"
checksum = "
b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487
"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.11.1",
"hashbrown 0.15.5",
"indexmap",
"semver",
"wit-bindgen",
]
[[package]]
...
...
@@ -1052,99 +939,11 @@ dependencies = [
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.11.1",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "xxhash-rust"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
...
...
@@ -1152,18 +951,18 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]]
name = "zerocopy"
version = "0.8.5
0
"
version = "0.8.5
2
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1
"
checksum = "
ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f
"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.5
0
"
version = "0.8.5
2
"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "
0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639
"
checksum = "
1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930
"
dependencies = [
"proc-macro2",
"quote",
...
...
projects/fastgpt-ide-agent/Cargo.toml
View file @
bc905bad
...
...
@@ -5,10 +5,10 @@ edition = "2024"
rust-version
=
"1.95"
[dependencies]
tokio
=
{
version
=
"1.52.3"
,
features
=
[
"macros"
,
"rt-multi-thread"
,
"net"
,
"time"
,
"sync"
,
"fs"
,
"io-util"
]
}
tokio
=
{
version
=
"1.52.3"
,
features
=
[
"macros"
,
"rt-multi-thread"
,
"net"
,
"time"
,
"sync"
,
"fs"
,
"io-util"
,
"process"
]
}
tokio-tungstenite
=
{
version
=
"0.29.0"
,
default-features
=
false
,
features
=
["handshake"]
}
portable-pty
=
"0.9.0"
serde
=
{
version
=
"1.0"
,
features
=
["derive"]
}
serde
=
{
version
=
"1.0
.228
"
,
features
=
["derive"]
}
serde_json
=
"1.0.150"
base64
=
"0.22.1"
futures-util
=
"0.3.32"
...
...
projects/fastgpt-ide-agent/src/connection.rs
View file @
bc905bad
...
...
@@ -2,11 +2,17 @@ use std::sync::Arc;
use
tokio
::
net
::
TcpStream
;
use
crate
::
fs
::
handle_fs_session
;
use
crate
::
fs
::
{
FsPermission
,
handle_fs_session
}
;
const
MAX_WS_MESSAGE_SIZE
:
usize
=
16
*
1024
*
1024
;
const
MAX_WS_FRAME_SIZE
:
usize
=
4
*
1024
*
1024
;
use
crate
::
terminal
::
handle_terminal_session
;
#[derive(Clone,
Copy,
Debug,
Eq,
PartialEq)]
enum
Channel
{
Fs
,
Terminal
,
}
fn
ws_config
()
->
tokio_tungstenite
::
tungstenite
::
protocol
::
WebSocketConfig
{
tokio_tungstenite
::
tungstenite
::
protocol
::
WebSocketConfig
::
default
()
.max_message_size
(
Some
(
MAX_WS_MESSAGE_SIZE
))
...
...
@@ -40,23 +46,26 @@ fn build_unauthorized_response(
resp
}
fn
parse_channel
(
path
:
&
str
)
->
Option
<
Channel
>
{
match
path
{
"/fs"
=>
Some
(
Channel
::
Fs
),
"/terminal"
=>
Some
(
Channel
::
Terminal
),
_
=>
None
,
}
}
#[allow(clippy
::
result_large_err)]
pub
async
fn
handle_connection
(
stream
:
TcpStream
,
expected_password
:
Arc
<
String
>
)
{
let
mut
path
=
String
::
new
()
;
let
mut
fs_permission
=
"write"
.to_string
()
;
let
mut
channel
=
None
;
let
mut
fs_permission
=
FsPermission
::
Read
;
let
ws_stream
=
match
tokio_tungstenite
::
accept_hdr_async_with_config
(
stream
,
|
req
:
&
tokio_tungstenite
::
tungstenite
::
handshake
::
server
::
Request
,
response
:
tokio_tungstenite
::
tungstenite
::
handshake
::
server
::
Response
|
{
path
=
req
.uri
()
.path
()
.to_string
();
let
expected_channel
=
if
path
.ends_with
(
"/terminal"
)
{
"terminal"
}
else
if
path
.ends_with
(
"/fs"
)
{
"fs"
}
else
{
return
Err
(
build_unauthorized_response
(
"Unknown websocket path"
));
};
let
parsed_channel
=
parse_channel
(
req
.uri
()
.path
())
.ok_or_else
(||
build_unauthorized_response
(
"Unknown websocket path"
))
?
;
channel
=
Some
(
parsed_channel
);
let
token_opt
=
extract_token
(
req
);
...
...
@@ -66,13 +75,13 @@ pub async fn handle_connection(stream: TcpStream, expected_password: Arc<String>
));
}
fs_permission
=
extract_query_value
(
req
,
"permission"
)
.unwrap_or_else
(||
"read"
.to_string
());
if
fs_permission
!=
"read"
&&
fs_permission
!=
"write"
{
return
Err
(
build_unauthorized_response
(
"Invalid fs permission"
));
}
fs_permission
=
match
extract_query_value
(
req
,
"permission"
)
{
Some
(
value
)
=>
FsPermission
::
parse
(
&
value
)
.ok_or_else
(||
build_unauthorized_response
(
"Invalid fs permission"
))
?
,
None
=>
FsPermission
::
Read
,
}
;
if
expected_channel
==
"terminal"
&&
fs_permission
!=
"write"
{
if
parsed_channel
==
Channel
::
Terminal
&&
fs_permission
!=
FsPermission
::
Write
{
return
Err
(
build_unauthorized_response
(
"Terminal connection requires write permission"
,
));
...
...
@@ -91,11 +100,9 @@ pub async fn handle_connection(stream: TcpStream, expected_password: Arc<String>
}
};
if
path
.ends_with
(
"/terminal"
)
{
handle_terminal_session
(
ws_stream
)
.await
;
}
else
if
path
.ends_with
(
"/fs"
)
{
handle_fs_session
(
ws_stream
,
fs_permission
)
.await
;
}
else
{
eprintln!
(
"Unknown request path for websocket: {}"
,
path
);
match
channel
{
Some
(
Channel
::
Terminal
)
=>
handle_terminal_session
(
ws_stream
)
.await
,
Some
(
Channel
::
Fs
)
=>
handle_fs_session
(
ws_stream
,
fs_permission
)
.await
,
None
=>
eprintln!
(
"Unknown request path after websocket handshake"
),
}
}
projects/fastgpt-ide-agent/src/fs.rs
View file @
bc905bad
use
std
::
collections
::{
BTreeSet
,
HashSet
};
use
std
::
path
::
Path
;
use
std
::
process
::
Stdio
;
use
std
::
sync
::{
Arc
,
OnceLock
};
use
std
::
time
::{
Duration
,
SystemTime
};
...
...
@@ -10,8 +11,11 @@ use notify::{Config, Event, EventKind, EventKindMask, RecommendedWatcher, Recurs
use
notify_debouncer_full
::{
DebounceEventResult
,
DebouncedEvent
,
RecommendedCache
,
new_debouncer_opt
,
};
use
serde
::
de
::
DeserializeOwned
;
use
serde
::{
Deserialize
,
Serialize
};
use
serde_json
::
json
;
use
tokio
::
io
::{
AsyncRead
,
AsyncReadExt
};
use
tokio
::
process
::
Command
;
use
tokio
::
sync
::{
broadcast
,
mpsc
};
use
tokio_tungstenite
::
tungstenite
::
Message
;
use
tokio_tungstenite
::
tungstenite
::
protocol
::
frame
::
CloseFrame
;
...
...
@@ -27,6 +31,99 @@ const FS_CHANGE_DEBOUNCE_MS: u64 = 500;
const
FS_CHANGE_MAX_PATHS
:
usize
=
200
;
const
FS_WATCH_BROADCAST_CAPACITY
:
usize
=
128
;
const
DEFAULT_MAX_FILE_BYTES
:
u64
=
10
*
1024
*
1024
;
const
EXEC_TIMEOUT_MS
:
u64
=
30
_000
;
const
EXEC_MAX_OUTPUT_BYTES
:
usize
=
1024
*
1024
;
const
DEFAULT_WORKSPACE_PATH
:
&
str
=
"."
;
const
DEFAULT_MAX_DEPTH
:
u64
=
20
;
// JSON-RPC 的 params 入口统一走强类型反序列化,避免各 handler 分散手写 Value 字段读取。
fn
parse_params
<
T
>
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
T
,
String
>
where
T
:
DeserializeOwned
,
{
let
params
=
params
.ok_or_else
(||
"Params required"
.to_string
())
?
;
serde_json
::
from_value
(
params
)
.map_err
(|
err
|
err
.to_string
())
}
fn
mtime_secs
(
metadata
:
&
std
::
fs
::
Metadata
)
->
u64
{
metadata
.modified
()
.unwrap_or
(
SystemTime
::
UNIX_EPOCH
)
.duration_since
(
SystemTime
::
UNIX_EPOCH
)
.unwrap_or_default
()
.as_secs
()
}
#[derive(Debug,
Deserialize)]
struct
PathParams
{
path
:
String
,
}
#[derive(Debug,
Deserialize)]
struct
WriteFileParams
{
path
:
String
,
content
:
String
,
#[serde(default,
rename
=
"old_mtime"
)]
old_mtime
:
Option
<
u64
>
,
}
#[derive(Debug,
Deserialize)]
#[serde(default,
rename_all
=
"camelCase"
)]
struct
ReadDirRecursiveParams
{
path
:
String
,
max_depth
:
u64
,
exclude_names
:
Option
<
Vec
<
String
>>
,
}
impl
Default
for
ReadDirRecursiveParams
{
fn
default
()
->
Self
{
Self
{
path
:
DEFAULT_WORKSPACE_PATH
.to_string
(),
max_depth
:
DEFAULT_MAX_DEPTH
,
exclude_names
:
None
,
}
}
}
#[derive(Debug,
Deserialize)]
struct
MoveParams
{
from
:
String
,
to
:
String
,
}
#[derive(Debug,
Deserialize)]
#[serde(rename_all
=
"camelCase"
)]
struct
ExecParams
{
command
:
String
,
#[serde(default)]
timeout_ms
:
Option
<
u64
>
,
}
impl
ExecParams
{
fn
timeout_ms
(
&
self
)
->
u64
{
self
.timeout_ms
.unwrap_or
(
EXEC_TIMEOUT_MS
)
}
}
#[derive(Clone,
Copy,
Debug,
Eq,
PartialEq)]
pub
enum
FsPermission
{
Read
,
Write
,
}
impl
FsPermission
{
pub
fn
parse
(
value
:
&
str
)
->
Option
<
Self
>
{
match
value
{
"read"
=>
Some
(
Self
::
Read
),
"write"
=>
Some
(
Self
::
Write
),
_
=>
None
,
}
}
fn
can_write
(
self
)
->
bool
{
matches!
(
self
,
Self
::
Write
)
}
}
#[derive(Debug,
Clone)]
struct
FsChangeBatch
{
...
...
@@ -42,14 +139,18 @@ struct FsWatchHub {
static
FS_WATCH_HUB
:
OnceLock
<
FsWatchHub
>
=
OnceLock
::
new
();
fn
max_file_bytes
()
->
u64
{
std
::
env
::
var
(
"FASTGPT_IDE_MAX_FILE_BYTES"
)
.ok
()
fn
parse_max_file_bytes
(
value
:
Option
<&
str
>
)
->
u64
{
value
.and_then
(|
value
|
value
.parse
::
<
u64
>
()
.ok
())
.filter
(|
value
|
*
value
>
0
)
.unwrap_or
(
DEFAULT_MAX_FILE_BYTES
)
}
fn
max_file_bytes
()
->
u64
{
let
value
=
std
::
env
::
var
(
"FASTGPT_IDE_MAX_FILE_BYTES"
)
.ok
();
parse_max_file_bytes
(
value
.as_deref
())
}
fn
default_exclude_names
()
->
HashSet
<
String
>
{
DEFAULT_EXCLUDED_NAMES
.iter
()
...
...
@@ -271,12 +372,8 @@ async fn forward_fs_change_batches(
}
async
fn
handle_read_dir
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
=
params
.ok_or
(
"Params required"
)
?
;
let
path_str
=
params
.get
(
"path"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"path param required"
)
?
;
let
clean_path
=
sanitize_path
(
path_str
)
.await
?
;
let
params
:
PathParams
=
parse_params
(
params
)
?
;
let
clean_path
=
sanitize_path
(
&
params
.path
)
.await
?
;
let
mut
entries
=
Vec
::
new
();
let
mut
dir
=
tokio
::
fs
::
read_dir
(
clean_path
)
...
...
@@ -288,12 +385,7 @@ async fn handle_read_dir(params: Option<serde_json::Value>) -> Result<serde_json
let
name
=
entry
.file_name
()
.to_string_lossy
()
.into_owned
();
let
is_dir
=
metadata
.is_dir
();
let
size
=
metadata
.len
();
let
mtime
=
metadata
.modified
()
.unwrap_or
(
SystemTime
::
UNIX_EPOCH
)
.duration_since
(
SystemTime
::
UNIX_EPOCH
)
.unwrap_or_default
()
.as_secs
();
let
mtime
=
mtime_secs
(
&
metadata
);
entries
.push
(
json!
({
"name"
:
name
,
...
...
@@ -307,12 +399,8 @@ async fn handle_read_dir(params: Option<serde_json::Value>) -> Result<serde_json
}
async
fn
handle_read_file
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
=
params
.ok_or
(
"Params required"
)
?
;
let
path_str
=
params
.get
(
"path"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"path param required"
)
?
;
let
clean_path
=
sanitize_path
(
path_str
)
.await
?
;
let
params
:
PathParams
=
parse_params
(
params
)
?
;
let
clean_path
=
sanitize_path
(
&
params
.path
)
.await
?
;
let
mut
file
=
tokio
::
fs
::
File
::
open
(
&
clean_path
)
.await
...
...
@@ -332,19 +420,13 @@ async fn handle_read_file(params: Option<serde_json::Value>) -> Result<serde_jso
}
let
mut
content_bytes
=
Vec
::
with_capacity
(
file_size
as
usize
);
use
tokio
::
io
::
AsyncReadExt
;
file
.read_to_end
(
&
mut
content_bytes
)
.await
.map_err
(|
e
|
e
.to_string
())
?
;
let
mut
content_b64
=
String
::
with_capacity
(
content_bytes
.len
()
.div_ceil
(
3
)
*
4
);
base64
::
engine
::
general_purpose
::
STANDARD
.encode_string
(
&
content_bytes
,
&
mut
content_b64
);
let
mtime
=
metadata
.modified
()
.unwrap_or
(
SystemTime
::
UNIX_EPOCH
)
.duration_since
(
SystemTime
::
UNIX_EPOCH
)
.unwrap_or_default
()
.as_secs
();
let
mtime
=
mtime_secs
(
&
metadata
);
Ok
(
json!
({
"content"
:
content_b64
,
...
...
@@ -353,33 +435,19 @@ async fn handle_read_file(params: Option<serde_json::Value>) -> Result<serde_jso
}
async
fn
handle_write_file
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
=
params
.ok_or
(
"Params required"
)
?
;
let
path_str
=
params
.get
(
"path"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"path param required"
)
?
;
let
content_b64
=
params
.get
(
"content"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"content param required"
)
?
;
let
clean_path
=
sanitize_create_path
(
path_str
)
.await
?
;
let
params
:
WriteFileParams
=
parse_params
(
params
)
?
;
let
clean_path
=
sanitize_create_path
(
&
params
.path
)
.await
?
;
if
clean_path
.exists
()
{
let
metadata
=
tokio
::
fs
::
metadata
(
&
clean_path
)
.await
.map_err
(|
e
|
e
.to_string
())
?
;
let
actual_mtime
=
metadata
.modified
()
.unwrap_or
(
SystemTime
::
UNIX_EPOCH
)
.duration_since
(
SystemTime
::
UNIX_EPOCH
)
.unwrap_or_default
()
.as_secs
();
let
actual_mtime
=
mtime_secs
(
&
metadata
);
// 校验修改时间防冲突
if
params
.get
(
"old_mtime"
)
.and_then
(|
v
|
v
.as_u64
())
.old_mtime
.is_some_and
(|
old_val
|
old_val
!=
actual_mtime
)
{
return
Err
(
"conflict"
.to_string
());
...
...
@@ -387,7 +455,7 @@ async fn handle_write_file(params: Option<serde_json::Value>) -> Result<serde_js
}
let
raw_bytes
=
base64
::
engine
::
general_purpose
::
STANDARD
.decode
(
content_b64
)
.decode
(
&
params
.content
)
.map_err
(|
e
|
e
.to_string
())
?
;
let
max_file_bytes
=
max_file_bytes
();
if
raw_bytes
.len
()
as
u64
>
max_file_bytes
{
...
...
@@ -418,12 +486,7 @@ async fn handle_write_file(params: Option<serde_json::Value>) -> Result<serde_js
.map_err
(|
e
|
e
.to_string
())
?
;
let
metadata
=
file
.metadata
()
.await
.map_err
(|
e
|
e
.to_string
())
?
;
let
new_mtime
=
metadata
.modified
()
.unwrap_or
(
SystemTime
::
UNIX_EPOCH
)
.duration_since
(
SystemTime
::
UNIX_EPOCH
)
.unwrap_or_default
()
.as_secs
();
let
new_mtime
=
mtime_secs
(
&
metadata
);
Ok
(
json!
({
"mtime"
:
new_mtime
}))
}
...
...
@@ -482,12 +545,7 @@ async fn scan_dir_recursive(
tasks
.push
(
async
move
{
let
metadata
=
entry
.metadata
()
.await
.map_err
(|
e
|
e
.to_string
())
?
;
let
size
=
metadata
.len
();
let
mtime
=
metadata
.modified
()
.unwrap_or
(
SystemTime
::
UNIX_EPOCH
)
.duration_since
(
SystemTime
::
UNIX_EPOCH
)
.unwrap_or_default
()
.as_secs
();
let
mtime
=
mtime_secs
(
&
metadata
);
let
mut
children
=
None
;
if
is_dir
&&
level
<
max_depth
{
...
...
@@ -540,25 +598,12 @@ async fn scan_dir_recursive(
async
fn
handle_read_dir_recursive
(
params
:
Option
<
serde_json
::
Value
>
,
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
=
params
.ok_or
(
"Params required"
)
?
;
let
path_str
=
params
.get
(
"path"
)
.and_then
(|
v
|
v
.as_str
())
.unwrap_or
(
"."
);
let
clean_path
=
sanitize_path
(
path_str
)
.await
?
;
let
max_depth
=
params
.get
(
"maxDepth"
)
.and_then
(|
v
|
v
.as_u64
())
.map
(|
depth
|
depth
.min
(
50
)
as
usize
)
.unwrap_or
(
20
);
let
params
:
ReadDirRecursiveParams
=
parse_params
(
params
)
?
;
let
clean_path
=
sanitize_path
(
&
params
.path
)
.await
?
;
let
max_depth
=
params
.max_depth
.min
(
50
)
as
usize
;
let
exclude_set
:
HashSet
<
String
>
=
params
.get
(
"excludeNames"
)
.and_then
(|
v
|
v
.as_array
())
.map
(|
items
|
{
items
.iter
()
.filter_map
(|
item
|
item
.as_str
())
.map
(
str
::
to_string
)
.collect
()
})
.exclude_names
.map
(|
items
|
items
.into_iter
()
.collect
())
.unwrap_or_else
(
default_exclude_names
);
let
exclude_names
=
Arc
::
new
(
exclude_set
);
...
...
@@ -581,12 +626,8 @@ async fn handle_read_dir_recursive(
}
async
fn
handle_mkdir
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
=
params
.ok_or
(
"Params required"
)
?
;
let
path_str
=
params
.get
(
"path"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"path param required"
)
?
;
let
clean_path
=
sanitize_create_path
(
path_str
)
.await
?
;
let
params
:
PathParams
=
parse_params
(
params
)
?
;
let
clean_path
=
sanitize_create_path
(
&
params
.path
)
.await
?
;
tokio
::
fs
::
create_dir_all
(
&
clean_path
)
.await
...
...
@@ -595,12 +636,8 @@ async fn handle_mkdir(params: Option<serde_json::Value>) -> Result<serde_json::V
}
async
fn
handle_delete
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
=
params
.ok_or
(
"Params required"
)
?
;
let
path_str
=
params
.get
(
"path"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"path param required"
)
?
;
let
clean_path
=
sanitize_path
(
path_str
)
.await
?
;
let
params
:
PathParams
=
parse_params
(
params
)
?
;
let
clean_path
=
sanitize_path
(
&
params
.path
)
.await
?
;
if
is_workspace_root_path
(
&
clean_path
)
{
return
Err
(
"Refusing to delete workspace root"
.to_string
());
}
...
...
@@ -618,18 +655,10 @@ async fn handle_delete(params: Option<serde_json::Value>) -> Result<serde_json::
}
async
fn
handle_move
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
=
params
.ok_or
(
"Params required"
)
?
;
let
from_str
=
params
.get
(
"from"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"from param required"
)
?
;
let
to_str
=
params
.get
(
"to"
)
.and_then
(|
v
|
v
.as_str
())
.ok_or
(
"to param required"
)
?
;
let
clean_from
=
sanitize_existing_workspace_entry_path
(
from_str
)
.await
?
;
let
clean_to
=
sanitize_create_path
(
to_str
)
.await
?
;
let
params
:
MoveParams
=
parse_params
(
params
)
?
;
let
clean_from
=
sanitize_existing_workspace_entry_path
(
&
params
.from
)
.await
?
;
let
clean_to
=
sanitize_create_path
(
&
params
.to
)
.await
?
;
if
is_workspace_root_path
(
&
clean_from
)
{
return
Err
(
"Refusing to move workspace root"
.to_string
());
}
...
...
@@ -651,15 +680,93 @@ async fn handle_move(params: Option<serde_json::Value>) -> Result<serde_json::Va
Ok
(
json!
({
"success"
:
true
}))
}
async
fn
read_limited_output
<
R
>
(
mut
reader
:
R
)
->
Result
<
Vec
<
u8
>
,
String
>
where
R
:
AsyncRead
+
Unpin
,
{
let
mut
output
=
Vec
::
new
();
let
mut
buffer
=
[
0u8
;
8192
];
loop
{
let
read_len
=
reader
.read
(
&
mut
buffer
)
.await
.map_err
(|
e
|
e
.to_string
())
?
;
if
read_len
==
0
{
break
;
}
// 达到返回上限后继续 drain pipe,避免子进程因 stdout/stderr 写满而卡住。
let
remaining
=
EXEC_MAX_OUTPUT_BYTES
.saturating_sub
(
output
.len
());
if
remaining
>
0
{
output
.extend_from_slice
(
&
buffer
[
..
read_len
.min
(
remaining
)]);
}
}
Ok
(
output
)
}
async
fn
handle_exec
(
params
:
Option
<
serde_json
::
Value
>
)
->
Result
<
serde_json
::
Value
,
String
>
{
let
params
:
ExecParams
=
parse_params
(
params
)
?
;
if
params
.command
.trim
()
.is_empty
()
{
return
Err
(
"command param required"
.to_string
());
}
let
timeout_ms
=
params
.timeout_ms
();
let
mut
child
=
Command
::
new
(
"sh"
)
.arg
(
"-lc"
)
.arg
(
params
.command
)
.current_dir
(
get_workspace_root
())
.stdin
(
Stdio
::
null
())
.stdout
(
Stdio
::
piped
())
.stderr
(
Stdio
::
piped
())
.kill_on_drop
(
true
)
.spawn
()
.map_err
(|
e
|
e
.to_string
())
?
;
let
stdout
=
child
.stdout
.take
()
.ok_or_else
(||
"Failed to capture command stdout"
.to_string
())
?
;
let
stderr
=
child
.stderr
.take
()
.ok_or_else
(||
"Failed to capture command stderr"
.to_string
())
?
;
let
output
=
match
tokio
::
time
::
timeout
(
Duration
::
from_millis
(
timeout_ms
),
async
{
let
(
status
,
stdout
,
stderr
)
=
tokio
::
try_join!
(
async
{
child
.wait
()
.await
.map_err
(|
e
|
e
.to_string
())
},
read_limited_output
(
stdout
),
read_limited_output
(
stderr
)
)
?
;
Ok
::
<
_
,
String
>
((
status
,
stdout
,
stderr
))
})
.await
{
Ok
(
output
)
=>
output
?
,
Err
(
_
)
=>
{
return
Ok
(
json!
({
"exitCode"
:
-
1
,
"stdout"
:
""
,
"stderr"
:
format!
(
"Command timed out after {}ms"
,
timeout_ms
),
}));
}
};
let
(
status
,
stdout
,
stderr
)
=
output
;
Ok
(
json!
({
"exitCode"
:
status
.code
()
.unwrap_or
(
-
1
),
"stdout"
:
String
::
from_utf8_lossy
(
&
stdout
)
.to_string
(),
"stderr"
:
String
::
from_utf8_lossy
(
&
stderr
)
.to_string
(),
}))
}
fn
is_write_fs_method
(
method
:
&
str
)
->
bool
{
matches!
(
method
,
"fs/write_file"
|
"fs/mkdir"
|
"fs/delete"
|
"fs/move"
"fs/write_file"
|
"fs/mkdir"
|
"fs/delete"
|
"fs/move"
|
"fs/exec"
)
}
async
fn
handle_fs_request
(
req
:
JsonRpcRequest
,
permission
:
&
str
)
->
JsonRpcResponse
{
if
permission
!=
"write"
&&
is_write_fs_method
(
req
.method
.as_str
())
{
async
fn
handle_fs_request
(
req
:
JsonRpcRequest
,
permission
:
FsPermission
)
->
JsonRpcResponse
{
if
!
permission
.can_write
()
&&
is_write_fs_method
(
req
.method
.as_str
())
{
return
JsonRpcResponse
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
req
.id
,
...
...
@@ -690,6 +797,7 @@ async fn handle_fs_request(req: JsonRpcRequest, permission: &str) -> JsonRpcResp
"fs/mkdir"
=>
handle_mkdir
(
req
.params
)
.await
.map_err
(|
e
|
(
-
32603
,
e
)),
"fs/delete"
=>
handle_delete
(
req
.params
)
.await
.map_err
(|
e
|
(
-
32603
,
e
)),
"fs/move"
=>
handle_move
(
req
.params
)
.await
.map_err
(|
e
|
(
-
32603
,
e
)),
"fs/exec"
=>
handle_exec
(
req
.params
)
.await
.map_err
(|
e
|
(
-
32603
,
e
)),
_
=>
Err
((
-
32601
,
"Method not found"
.to_string
())),
};
...
...
@@ -711,7 +819,7 @@ async fn handle_fs_request(req: JsonRpcRequest, permission: &str) -> JsonRpcResp
pub
async
fn
handle_fs_session
<
S
>
(
ws_stream
:
tokio_tungstenite
::
WebSocketStream
<
S
>
,
permission
:
String
,
permission
:
FsPermission
,
)
where
S
:
tokio
::
io
::
AsyncRead
+
tokio
::
io
::
AsyncWrite
+
Unpin
+
Send
+
'static
,
{
...
...
@@ -767,7 +875,7 @@ pub async fn handle_fs_session<S>(
continue
;
};
let
response
=
handle_fs_request
(
req
,
&
permission
)
.await
;
let
response
=
handle_fs_request
(
req
,
permission
)
.await
;
if
let
Ok
(
response_text
)
=
serde_json
::
to_string
(
&
response
)
&&
outbound_tx
.send
(
Message
::
Text
(
response_text
.into
()))
...
...
@@ -811,101 +919,85 @@ mod tests {
use
super
::
*
;
use
crate
::
workspace
::
init_test_workspace
;
use
notify
::
event
::{
CreateKind
,
EventAttributes
,
EventKind
};
use
serde_json
::
json
;
use
serde_json
::
{
Value
,
json
}
;
use
std
::
fs
;
use
std
::
path
::
PathBuf
;
use
std
::
time
::
Instant
;
fn
fs_request
(
method
:
&
str
,
params
:
Option
<
Value
>
)
->
JsonRpcRequest
{
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
json!
(
1
),
method
:
method
.to_string
(),
params
,
}
}
async
fn
fs_ok
(
method
:
&
str
,
params
:
Value
)
->
Value
{
let
resp
=
handle_fs_request
(
fs_request
(
method
,
Some
(
params
)),
FsPermission
::
Write
)
.await
;
match
(
resp
.result
,
resp
.error
)
{
(
Some
(
result
),
None
)
=>
result
,
(
_
,
Some
(
error
))
=>
panic!
(
"unexpected fs error for {method}: {error:?}"
),
(
None
,
None
)
=>
panic!
(
"missing fs result for {method}"
),
}
}
async
fn
fs_err
(
method
:
&
str
,
params
:
Option
<
Value
>
,
permission
:
FsPermission
)
->
JsonRpcError
{
let
resp
=
handle_fs_request
(
fs_request
(
method
,
params
),
permission
)
.await
;
match
(
resp
.result
,
resp
.error
)
{
(
None
,
Some
(
error
))
=>
error
,
(
Some
(
result
),
_
)
=>
panic!
(
"unexpected fs result for {method}: {result:?}"
),
(
None
,
None
)
=>
panic!
(
"missing fs error for {method}"
),
}
}
fn
notify_event
(
kind
:
EventKind
,
paths
:
Vec
<
PathBuf
>
)
->
Event
{
Event
{
kind
,
paths
,
attrs
:
EventAttributes
::
new
(),
}
}
#[tokio
::
test]
async
fn
test_
handle_fs_request_jsonrpc
_workflow
()
{
async
fn
test_
fs_crud_and_tree
_workflow
()
{
let
temp_workspace
=
init_test_workspace
();
let
_
=
fs
::
remove_dir_all
(
temp_workspace
.join
(
"src_test"
));
// 1. 测试创建目录 (fs/mkdir)
let
mkdir_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
1
.into
()),
method
:
"fs/mkdir"
.to_string
(),
params
:
Some
(
json!
({
"path"
:
"src_test"
})),
};
let
resp
=
handle_fs_request
(
mkdir_req
,
"write"
)
.await
;
assert
!
(
resp
.error
.is_none
());
assert_eq!
(
resp
.result
.unwrap
()
.get
(
"success"
)
.unwrap
()
.as_bool
(),
Some
(
true
)
);
let
mkdir_result
=
fs_ok
(
"fs/mkdir"
,
json!
({
"path"
:
"src_test"
}))
.await
;
assert_eq!
(
mkdir_result
[
"success"
],
json!
(
true
));
// 2. 测试写入文件 (fs/write_file) - Base64 编码的 "Hello Rust!" 是 "SGVsbG8gUnVzdCE="
let
write_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
2
.into
()),
method
:
"fs/write_file"
.to_string
(),
params
:
Some
(
json!
({
let
write_result
=
fs_ok
(
"fs/write_file"
,
json!
({
"path"
:
"src_test/hello.txt"
,
"content"
:
"SGVsbG8gUnVzdCE="
})),
};
let
resp
=
handle_fs_request
(
write_req
,
"write"
)
.await
;
assert
!
(
resp
.error
.is_none
());
let
mtime
=
resp
.result
.unwrap
()
.get
(
"mtime"
)
.unwrap
()
.as_u64
();
assert
!
(
mtime
.is_some
());
}),
)
.await
;
assert
!
(
write_result
[
"mtime"
]
.as_u64
()
.is_some
());
// 2.1 测试写入嵌套缺失父目录时会自动创建父目录
let
nested_write_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
21
.into
()),
method
:
"fs/write_file"
.to_string
(),
params
:
Some
(
json!
({
fs_ok
(
"fs/write_file"
,
json!
({
"path"
:
"missing_parent/a/hello.txt"
,
"content"
:
"SGVsbG8gUnVzdCE="
})),
};
let
resp
=
handle_fs_request
(
nested_write_req
,
"write"
)
.await
;
assert
!
(
resp
.error
.is_none
());
}),
)
.await
;
assert
!
(
temp_workspace
.join
(
"missing_parent/a/hello.txt"
)
.exists
());
// 3. 测试读取文件 (fs/read_file)
let
read_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
3
.into
()),
method
:
"fs/read_file"
.to_string
(),
params
:
Some
(
json!
({
"path"
:
"src_test/hello.txt"
})),
};
let
resp
=
handle_fs_request
(
read_req
,
"write"
)
.await
;
assert
!
(
resp
.error
.is_none
());
let
result_obj
=
resp
.result
.unwrap
();
let
content_b64
=
result_obj
.get
(
"content"
)
.unwrap
()
.as_str
()
.unwrap
();
assert_eq!
(
content_b64
,
"SGVsbG8gUnVzdCE="
);
// 4. 测试递归读取文件树 (fs/read_dir_recursive)
let
read_tree_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
4
.into
()),
method
:
"fs/read_dir_recursive"
.to_string
(),
params
:
Some
(
json!
({
"path"
:
"."
})),
};
let
resp
=
handle_fs_request
(
read_tree_req
,
"write"
)
.await
;
assert
!
(
resp
.error
.is_none
());
let
tree_res
=
resp
.result
.unwrap
();
let
files
=
tree_res
.get
(
"files"
)
.unwrap
()
.as_array
()
.unwrap
();
let
read_result
=
fs_ok
(
"fs/read_file"
,
json!
({
"path"
:
"src_test/hello.txt"
}))
.await
;
assert_eq!
(
read_result
[
"content"
],
json!
(
"SGVsbG8gUnVzdCE="
));
let
tree_result
=
fs_ok
(
"fs/read_dir_recursive"
,
json!
({
"path"
:
"."
}))
.await
;
let
files
=
tree_result
[
"files"
]
.as_array
()
.unwrap
();
assert
!
(
!
files
.is_empty
());
let
src_node
=
files
.iter
()
.find
(|
node
|
node
.get
(
"name"
)
.and_then
(|
value
|
value
.as_str
())
==
Some
(
"src_test"
))
.find
(|
node
|
node
[
"name"
]
==
json!
(
"src_test"
))
.expect
(
"src_test node should be present"
);
assert_eq!
(
src_node
.get
(
"type"
)
.unwrap
()
.as_str
(),
Some
(
"directory"
));
// 5. 测试非法方法名 (fs/invalid_method) - 应返回标准错误
let
invalid_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
5
.into
()),
method
:
"fs/invalid_method"
.to_string
(),
params
:
None
,
};
let
resp
=
handle_fs_request
(
invalid_req
,
"write"
)
.await
;
assert
!
(
resp
.result
.is_none
());
let
err
=
resp
.error
.unwrap
();
assert_eq!
(
err
.code
,
-
32601
);
assert
!
(
err
.message
.contains
(
"Method not found"
));
assert_eq!
(
src_node
[
"type"
],
json!
(
"directory"
));
}
#[tokio
::
test]
...
...
@@ -913,28 +1005,18 @@ mod tests {
let
temp_workspace
=
init_test_workspace
();
let
_
=
fs
::
remove_dir_all
(
temp_workspace
.join
(
"nested_create_test"
));
let
mkdir_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
1
.into
()),
method
:
"fs/mkdir"
.to_string
(),
params
:
Some
(
json!
({
"path"
:
"nested_create_test/a/b"
})),
};
let
resp
=
handle_fs_request
(
mkdir_req
,
"write"
)
.await
;
assert
!
(
resp
.error
.is_none
());
fs_ok
(
"fs/mkdir"
,
json!
({
"path"
:
"nested_create_test/a/b"
}))
.await
;
assert
!
(
temp_workspace
.join
(
"nested_create_test/a/b"
)
.is_dir
());
fs
::
write
(
temp_workspace
.join
(
"nested_create_test/source.txt"
),
"move"
)
.unwrap
();
let
move_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
2
.into
()),
method
:
"fs/move"
.to_string
(),
params
:
Some
(
json!
({
fs_ok
(
"fs/move"
,
json!
({
"from"
:
"nested_create_test/source.txt"
,
"to"
:
"nested_create_test/c/d/target.txt"
})),
};
let
resp
=
handle_fs_request
(
move_req
,
"write"
)
.await
;
assert
!
(
resp
.error
.is_none
());
}),
)
.await
;
assert
!
(
temp_workspace
.join
(
"nested_create_test/c/d/target.txt"
)
...
...
@@ -954,18 +1036,15 @@ mod tests {
let
_
=
fs
::
remove_dir_all
(
&
moved_link_path
);
std
::
os
::
unix
::
fs
::
symlink
(
outside
.path
(),
&
link_path
)
.unwrap
();
let
move_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
1
.into
()),
method
:
"fs/move"
.to_string
(),
params
:
Some
(
json!
({
fs_ok
(
"fs/move"
,
json!
({
"from"
:
"move_symlink_source"
,
"to"
:
"move_symlink_target"
})
)
,
};
let
resp
=
handle_fs_request
(
move_req
,
"write"
)
.await
;
}),
)
.await
;
assert
!
(
resp
.error
.is_none
());
assert
!
(
!
link_path
.exists
());
assert
!
(
fs
::
symlink_metadata
(
&
moved_link_path
)
...
...
@@ -980,70 +1059,108 @@ mod tests {
async
fn
test_read_only_ticket_rejects_write_methods
()
{
let
_temp_workspace
=
init_test_workspace
();
let
write_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
1
.into
()),
method
:
"fs/write_file"
.to_string
(),
params
:
Some
(
json!
({
let
write_err
=
fs_err
(
"fs/write_file"
,
Some
(
json!
({
"path"
:
"readonly.txt"
,
"content"
:
"SGVsbG8="
})),
};
let
resp
=
handle_fs_request
(
write_req
,
"read"
)
.await
;
assert
!
(
resp
.result
.is_none
());
let
err
=
resp
.error
.unwrap
();
assert_eq!
(
err
.code
,
-
32003
);
assert
!
(
err
.message
.contains
(
"read-only"
));
FsPermission
::
Read
,
)
.await
;
assert_eq!
(
write_err
.code
,
-
32003
);
assert
!
(
write_err
.message
.contains
(
"read-only"
));
let
exec_err
=
fs_err
(
"fs/exec"
,
Some
(
json!
({
"command"
:
"echo denied"
})),
FsPermission
::
Read
,
)
.await
;
assert_eq!
(
exec_err
.code
,
-
32003
);
assert
!
(
exec_err
.message
.contains
(
"read-only"
));
}
#[tokio
::
test]
async
fn
test_delete_and_move_reject_workspace_root
()
{
let
_temp_workspace
=
init_test_workspace
();
let
delete_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
1
.into
()),
method
:
"fs/delete"
.to_string
(),
params
:
Some
(
json!
({
"path"
:
"."
})),
};
let
resp
=
handle_fs_request
(
delete_req
,
"write"
)
.await
;
assert
!
(
resp
.result
.is_none
());
assert
!
(
resp
.error
.unwrap
()
.message
.contains
(
"workspace root"
));
let
delete_err
=
fs_err
(
"fs/delete"
,
Some
(
json!
({
"path"
:
"."
})),
FsPermission
::
Write
,
)
.await
;
assert
!
(
delete_err
.message
.contains
(
"workspace root"
));
let
move_err
=
fs_err
(
"fs/move"
,
Some
(
json!
({
"from"
:
"."
,
"to"
:
"moved-root"
})),
FsPermission
::
Write
,
)
.await
;
assert
!
(
move_err
.message
.contains
(
"workspace root"
));
}
let
move_req
=
JsonRpcRequest
{
jsonrpc
:
"2.0"
.to_string
(),
id
:
serde_json
::
Value
::
Number
(
2
.into
()),
method
:
"fs/move"
.to_string
(),
params
:
Some
(
json!
({
"from"
:
"."
,
"to"
:
"moved-root"
})),
};
let
resp
=
handle_fs_request
(
move_req
,
"write"
)
.await
;
assert
!
(
resp
.result
.is_none
());
assert
!
(
resp
.error
.unwrap
()
.message
.contains
(
"workspace root"
));
#[tokio
::
test]
async
fn
test_unknown_fs_method_returns_jsonrpc_error
()
{
let
_temp_workspace
=
init_test_workspace
();
let
err
=
fs_err
(
"fs/invalid_method"
,
None
,
FsPermission
::
Write
)
.await
;
assert_eq!
(
err
.code
,
-
32601
);
assert
!
(
err
.message
.contains
(
"Method not found"
));
}
#[test]
fn
test_max_file_bytes_reads_positive_env
()
{
unsafe
{
std
::
env
::
set_var
(
"FASTGPT_IDE_MAX_FILE_BYTES"
,
"2048"
);
}
assert_eq!
(
max_file_bytes
(),
2048
);
unsafe
{
std
::
env
::
remove_var
(
"FASTGPT_IDE_MAX_FILE_BYTES"
);
}
fn
test_parse_max_file_bytes
()
{
assert_eq!
(
parse_max_file_bytes
(
Some
(
"2048"
)),
2048
);
assert_eq!
(
parse_max_file_bytes
(
Some
(
"0"
)),
DEFAULT_MAX_FILE_BYTES
);
assert_eq!
(
parse_max_file_bytes
(
Some
(
"bad"
)),
DEFAULT_MAX_FILE_BYTES
);
assert_eq!
(
parse_max_file_bytes
(
None
),
DEFAULT_MAX_FILE_BYTES
);
}
#[tokio
::
test]
async
fn
test_exec_runs_in_workspace_and_returns_output
()
{
let
temp_workspace
=
init_test_workspace
();
let
result
=
fs_ok
(
"fs/exec"
,
json!
({
"command"
:
"pwd && printf done"
}))
.await
;
assert_eq!
(
result
[
"exitCode"
],
json!
(
0
));
let
stdout
=
result
[
"stdout"
]
.as_str
()
.unwrap
();
assert
!
(
stdout
.contains
(
temp_workspace
.to_str
()
.unwrap
()));
assert
!
(
stdout
.contains
(
"done"
));
}
#[tokio
::
test]
async
fn
test_exec_returns_non_zero_exit_code_and_stderr
()
{
let
_temp_workspace
=
init_test_workspace
();
let
result
=
fs_ok
(
"fs/exec"
,
json!
({
"command"
:
"printf err >&2; exit 7"
}))
.await
;
assert_eq!
(
result
[
"exitCode"
],
json!
(
7
));
assert_eq!
(
result
[
"stderr"
],
json!
(
"err"
));
}
#[tokio
::
test]
async
fn
test_exec_respects_timeout_ms
()
{
let
_temp_workspace
=
init_test_workspace
();
let
result
=
fs_ok
(
"fs/exec"
,
json!
({
"command"
:
"sleep 2"
,
"timeoutMs"
:
1
}))
.await
;
assert_eq!
(
result
[
"exitCode"
],
json!
(
-
1
));
assert
!
(
result
[
"stderr"
]
.as_str
()
.unwrap
()
.contains
(
"timed out"
));
}
#[test]
fn
test_collect_fs_event_paths_normalizes_paths
()
{
let
temp_workspace
=
init_test_workspace
();
let
event
=
Event
{
kind
:
EventKind
::
Create
(
CreateKind
::
File
),
paths
:
vec!
[
let
event
=
notify_event
(
EventKind
::
Create
(
CreateKind
::
File
),
vec!
[
temp_workspace
.join
(
"src"
)
.join
(
"index.ts"
),
temp_workspace
.join
(
"package.json"
),
],
attrs
:
EventAttributes
::
new
(),
};
);
let
paths
=
collect_fs_event_paths
(
&
event
);
assert_eq!
(
paths
,
vec!
[
"src/index.ts"
,
"package.json"
]);
...
...
@@ -1052,9 +1169,9 @@ mod tests {
#[test]
fn
test_collect_fs_event_paths_does_not_hardcode_common_build_dirs
()
{
let
temp_workspace
=
init_test_workspace
();
let
event
=
Event
{
kind
:
EventKind
::
Create
(
CreateKind
::
File
),
paths
:
vec!
[
let
event
=
notify_event
(
EventKind
::
Create
(
CreateKind
::
File
),
vec!
[
temp_workspace
.join
(
"node_modules"
)
.join
(
"pkg"
)
...
...
@@ -1063,8 +1180,7 @@ mod tests {
temp_workspace
.join
(
"dist"
)
.join
(
"index.js"
),
temp_workspace
.join
(
"build"
)
.join
(
"index.js"
),
],
attrs
:
EventAttributes
::
new
(),
};
);
let
paths
=
collect_fs_event_paths
(
&
event
);
assert_eq!
(
...
...
@@ -1082,13 +1198,12 @@ mod tests {
fn
test_collect_debounced_event_paths_ignores_access_events
()
{
let
temp_workspace
=
init_test_workspace
();
let
events
=
vec!
[
DebouncedEvent
::
new
(
Event
{
kind
:
EventKind
::
Access
(
notify
::
event
::
AccessKind
::
Open
(
notify_event
(
EventKind
::
Access
(
notify
::
event
::
AccessKind
::
Open
(
notify
::
event
::
AccessMode
::
Read
,
)),
paths
:
vec!
[
temp_workspace
.join
(
"skills"
)],
attrs
:
EventAttributes
::
new
(),
},
vec!
[
temp_workspace
.join
(
"skills"
)],
),
Instant
::
now
(),
)];
...
...
projects/fastgpt-ide-agent/src/main.rs
View file @
bc905bad
...
...
@@ -22,7 +22,11 @@ async fn main() {
);
if
!
workspace
.exists
()
{
let
_
=
tokio
::
fs
::
create_dir_all
(
workspace
)
.await
;
tokio
::
fs
::
create_dir_all
(
workspace
)
.await
.unwrap_or_else
(|
err
|
{
panic!
(
"Failed to create workspace root {:?}: {}"
,
workspace
,
err
)
});
}
let
password
=
Arc
::
new
(
...
...
projects/fastgpt-ide-agent/src/password.rs
View file @
bc905bad
...
...
@@ -33,15 +33,21 @@ pub fn load_or_create_ide_agent_password() -> Result<String, String> {
let
password_path
=
get_password_path
();
let
password_path
=
password_path
.as_path
();
match
std
::
fs
::
read_to_string
(
password_path
)
{
let
read_existing_password
=
||
match
std
::
fs
::
read_to_string
(
password_path
)
{
Ok
(
content
)
=>
{
let
password
=
content
.trim
()
.to_string
();
if
!
password
.is_empty
()
{
return
Ok
(
password
);
Ok
(
Some
(
password
))
}
else
{
Ok
(
None
)
}
}
Err
(
err
)
if
err
.kind
()
==
std
::
io
::
ErrorKind
::
NotFound
=>
{}
Err
(
err
)
=>
return
Err
(
format!
(
"Failed to read IDE Agent password file: {}"
,
err
)),
Err
(
err
)
if
err
.kind
()
==
std
::
io
::
ErrorKind
::
NotFound
=>
Ok
(
None
),
Err
(
err
)
=>
Err
(
format!
(
"Failed to read IDE Agent password file: {}"
,
err
)),
};
if
let
Some
(
password
)
=
read_existing_password
()
?
{
return
Ok
(
password
);
}
let
parent
=
password_path
...
...
@@ -52,14 +58,19 @@ pub fn load_or_create_ide_agent_password() -> Result<String, String> {
let
password
=
generate_ide_agent_password
();
let
mut
options
=
OpenOptions
::
new
();
options
.write
(
true
)
.create
(
true
)
.truncate
(
true
);
options
.write
(
true
)
.create
_new
(
true
);
#[cfg(unix)]
{
options
.mode
(
0
o600
);
}
let
mut
file
=
options
.open
(
password_path
)
.map_err
(|
err
|
format!
(
"Failed to create IDE Agent password file: {}"
,
err
))
?
;
let
mut
file
=
match
options
.open
(
password_path
)
{
Ok
(
file
)
=>
file
,
Err
(
err
)
if
err
.kind
()
==
std
::
io
::
ErrorKind
::
AlreadyExists
=>
{
return
read_existing_password
()
?
.ok_or_else
(||
"IDE Agent password file exists but is empty"
.to_string
());
}
Err
(
err
)
=>
return
Err
(
format!
(
"Failed to create IDE Agent password file: {}"
,
err
)),
};
file
.write_all
(
format!
(
"{}
\n
"
,
password
)
.as_bytes
())
.map_err
(|
err
|
format!
(
"Failed to write IDE Agent password file: {}"
,
err
))
?
;
...
...
projects/fastgpt-ide-agent/src/workspace.rs
View file @
bc905bad
...
...
@@ -189,40 +189,41 @@ pub fn init_test_workspace() -> &'static Path {
mod
tests
{
use
super
::
*
;
use
std
::
fs
;
use
std
::
path
::
PathBuf
;
fn
assert_path_ok
(
result
:
Result
<
PathBuf
,
String
>
,
expected_suffix
:
&
str
)
->
PathBuf
{
let
path
=
result
.expect
(
"path should be valid"
);
assert
!
(
path
.ends_with
(
expected_suffix
));
path
}
fn
assert_path_err
(
result
:
Result
<
PathBuf
,
String
>
)
{
assert
!
(
result
.is_err
());
}
#[tokio
::
test]
async
fn
test_sanitize_path_success
()
{
let
temp_workspace
=
init_test_workspace
();
// 写入一个虚拟测试文件,以确保 validate_path 能正常通过
let
test_file
=
temp_workspace
.join
(
"dummy.txt"
);
fs
::
write
(
&
test_file
,
"dummy"
)
.unwrap
();
let
res
=
sanitize_path
(
"dummy.txt"
)
.await
;
assert
!
(
res
.is_ok
());
let
path
=
res
.unwrap
();
assert
!
(
path
.ends_with
(
"dummy.txt"
));
let
res
=
sanitize_path
(
"./dummy.txt"
)
.await
;
assert
!
(
res
.is_ok
());
let
path
=
res
.unwrap
();
assert
!
(
path
.ends_with
(
"dummy.txt"
));
assert_path_ok
(
sanitize_path
(
"dummy.txt"
)
.await
,
"dummy.txt"
);
assert_path_ok
(
sanitize_path
(
"./dummy.txt"
)
.await
,
"dummy.txt"
);
}
#[tokio
::
test]
async
fn
test_sanitize_path_absolute_denied
()
{
let
_temp_workspace
=
init_test_workspace
();
let
res
=
sanitize_path
(
"/dummy.txt"
)
.await
;
assert
!
(
res
.is_err
());
assert_path_err
(
sanitize_path
(
"/dummy.txt"
)
.await
);
}
#[tokio
::
test]
async
fn
test_sanitize_path_traversal_denied
()
{
let
_temp_workspace
=
init_test_workspace
();
let
res
=
sanitize_path
(
"../../../etc/passwd"
)
.await
;
assert
!
(
res
.is_err
());
assert_path_err
(
sanitize_path
(
"../../../etc/passwd"
)
.await
);
}
#[tokio
::
test]
...
...
@@ -231,17 +232,17 @@ mod tests {
let
target
=
temp_workspace
.join
(
"nested_missing_parent"
);
let
_
=
fs
::
remove_dir_all
(
&
target
);
let
res
=
sanitize_create_path
(
"nested_missing_parent/a/file.txt"
)
.await
;
assert
!
(
res
.is_ok
());
assert
!
(
res
.unwrap
()
.ends_with
(
"nested_missing_parent/a/file.txt"
));
assert_path_ok
(
sanitize_create_path
(
"nested_missing_parent/a/file.txt"
)
.await
,
"nested_missing_parent/a/file.txt"
,
);
}
#[tokio
::
test]
async
fn
test_sanitize_create_path_traversal_denied
()
{
let
_temp_workspace
=
init_test_workspace
();
let
res
=
sanitize_create_path
(
"nested/../../../etc/passwd"
)
.await
;
assert
!
(
res
.is_err
());
assert_path_err
(
sanitize_create_path
(
"nested/../../../etc/passwd"
)
.await
);
}
#[cfg(unix)]
...
...
@@ -253,9 +254,10 @@ mod tests {
let
_
=
fs
::
remove_file
(
&
link_path
);
std
::
os
::
unix
::
fs
::
symlink
(
outside
.path
(),
&
link_path
)
.unwrap
();
let
res
=
sanitize_existing_workspace_entry_path
(
"move_source_link"
)
.await
.unwrap
();
let
res
=
assert_path_ok
(
sanitize_existing_workspace_entry_path
(
"move_source_link"
)
.await
,
"move_source_link"
,
);
assert_eq!
(
res
.file_name
(),
link_path
.file_name
());
assert_ne!
(
res
,
outside
.path
());
...
...
@@ -270,7 +272,6 @@ mod tests {
let
_
=
fs
::
remove_file
(
&
link_path
);
std
::
os
::
unix
::
fs
::
symlink
(
outside
.path
(),
&
link_path
)
.unwrap
();
let
res
=
sanitize_create_path
(
"outside_link_for_create/file.txt"
)
.await
;
assert
!
(
res
.is_err
());
assert_path_err
(
sanitize_create_path
(
"outside_link_for_create/file.txt"
)
.await
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment