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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ export class RpcApiType {
return client.wshRpcCall("filerestorebackup", data, opts);
}

// command "filestream" [call]
FileStreamCommand(client: WshClient, data: CommandFileStreamData, opts?: RpcOpts): Promise<FileInfo> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "filestream", data, opts);
return client.wshRpcCall("filestream", data, opts);
}

// command "filewrite" [call]
FileWriteCommand(client: WshClient, data: FileData, opts?: RpcOpts): Promise<void> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "filewrite", data, opts);
Expand Down Expand Up @@ -720,6 +726,12 @@ export class RpcApiType {
return client.wshRpcCall("remotefilemultiinfo", data, opts);
}

// command "remotefilestream" [call]
RemoteFileStreamCommand(client: WshClient, data: CommandRemoteFileStreamData, opts?: RpcOpts): Promise<FileInfo> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remotefilestream", data, opts);
return client.wshRpcCall("remotefilestream", data, opts);
}

// command "remotefiletouch" [call]
RemoteFileTouchCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remotefiletouch", data, opts);
Expand Down
8 changes: 2 additions & 6 deletions frontend/app/view/preview/preview-streaming.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,14 @@ function StreamingPreview({ model }: SpecializedViewProps) {
if (fileInfo.mimetype.startsWith("video/")) {
return (
<div className="flex flex-row h-full overflow-hidden items-center justify-center">
<video controls className="w-full h-full p-[10px] object-contain">
<source src={streamingUrl} />
</video>
<video controls src={streamingUrl} className="w-full h-full p-[10px] object-contain" />
</div>
);
}
if (fileInfo.mimetype.startsWith("audio/")) {
return (
<div className="flex flex-row h-full overflow-hidden items-center justify-center">
<audio controls className="w-full h-full p-[10px] object-contain">
<source src={streamingUrl} />
</audio>
<audio controls src={streamingUrl} className="w-full h-full p-[10px] object-contain" />
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/view/term/termwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ export class TermWrap {
}
}
let resolve: () => void = null;
let prtn = new Promise<void>((presolve, _) => {
const prtn = new Promise<void>((presolve, _) => {
resolve = presolve;
});
this.terminal.write(data, () => {
Expand Down
14 changes: 14 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,13 @@ declare global {
restoretofilename: string;
};

// wshrpc.CommandFileStreamData
type CommandFileStreamData = {
info: FileInfo;
byterange?: string;
streammeta: StreamMeta;
};

// wshrpc.CommandGetMetaData
type CommandGetMetaData = {
oref: ORef;
Expand Down Expand Up @@ -521,6 +528,13 @@ declare global {
paths: string[];
};

// wshrpc.CommandRemoteFileStreamData
type CommandRemoteFileStreamData = {
path: string;
byterange?: string;
streammeta: StreamMeta;
};

// wshrpc.CommandRemoteListEntriesData
type CommandRemoteListEntriesData = {
path: string;
Expand Down
27 changes: 26 additions & 1 deletion pkg/remote/fileshare/wshfs/wshfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,37 @@ func ReadStream(ctx context.Context, data wshrpc.FileData) <-chan wshrpc.RespOrE
func readStream(conn *connparse.Connection, data wshrpc.FileData) <-chan wshrpc.RespOrErrorUnion[wshrpc.FileData] {
byteRange := ""
if data.At != nil && data.At.Size > 0 {
byteRange = fmt.Sprintf("%d-%d", data.At.Offset, data.At.Offset+int64(data.At.Size))
byteRange = fmt.Sprintf("%d-%d", data.At.Offset, data.At.Offset+int64(data.At.Size)-1)
}
streamFileData := wshrpc.CommandRemoteStreamFileData{Path: conn.Path, ByteRange: byteRange}
return wshclient.RemoteStreamFileCommand(RpcClient, streamFileData, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(conn.Host)})
}

func GetConnectionRouteId(ctx context.Context, path string) (string, error) {
conn, err := parseConnection(ctx, path)
if err != nil {
return "", err
}
return wshutil.MakeConnectionRouteId(conn.Host), nil
}

func FileStream(ctx context.Context, data wshrpc.CommandFileStreamData) (*wshrpc.FileInfo, error) {
if data.Info == nil {
return nil, fmt.Errorf("file info is required")
}
log.Printf("FileStream: %v", data.Info.Path)
conn, err := parseConnection(ctx, data.Info.Path)
if err != nil {
return nil, err
}
remoteData := wshrpc.CommandRemoteFileStreamData{
Path: conn.Path,
ByteRange: data.ByteRange,
StreamMeta: data.StreamMeta,
}
return wshclient.RemoteFileStreamCommand(RpcClient, remoteData, &wshrpc.RpcOpts{Route: wshutil.MakeConnectionRouteId(conn.Host)})
}

func ListEntries(ctx context.Context, path string, opts *wshrpc.FileListOpts) ([]*wshrpc.FileInfo, error) {
log.Printf("ListEntries: %v", path)
conn, err := parseConnection(ctx, path)
Expand Down
35 changes: 34 additions & 1 deletion pkg/util/fileutil/fileutil.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Copyright 2025, Command Line Inc.
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package fileutil

import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
Expand All @@ -18,6 +19,38 @@ import (
"github.com/wavetermdev/waveterm/pkg/wavebase"
)

type ByteRangeType struct {
All bool
Start int64
End int64 // inclusive; only valid when OpenEnd is false
OpenEnd bool // true when range is "N-" (read from Start to EOF)
}

func ParseByteRange(rangeStr string) (ByteRangeType, error) {
if rangeStr == "" {
return ByteRangeType{All: true}, nil
}
// handle open-ended range "N-"
if len(rangeStr) > 0 && rangeStr[len(rangeStr)-1] == '-' {
var start int64
_, err := fmt.Sscanf(rangeStr, "%d-", &start)
if err != nil || start < 0 {
return ByteRangeType{}, errors.New("invalid byte range")
}
return ByteRangeType{Start: start, OpenEnd: true}, nil
}
var start, end int64
_, err := fmt.Sscanf(rangeStr, "%d-%d", &start, &end)
if err != nil {
return ByteRangeType{}, errors.New("invalid byte range")
}
if start < 0 || end < 0 || start > end {
return ByteRangeType{}, errors.New("invalid byte range")
}
// End is inclusive (HTTP byte range semantics: bytes=0-999 means 1000 bytes)
return ByteRangeType{Start: start, End: end}, nil
Comment on lines +29 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does Go's fmt.Sscanf return success when the format matches but trailing input remains unread?

💡 Result:

Yes, Go's fmt.Sscanf returns success (n > 0 and err == nil) when the format matches and parses the expected values, even if trailing input remains unread. It does not require consuming the entire input string and only signals an error if the format fails to match or parsing fails.

Citations:


Reject trailing garbage in ParseByteRange.

fmt.Sscanf succeeds when the format matches and parses the expected values, even if trailing input remains unread. This means inputs like 0-99,200-299 or 0-99foo are silently accepted as 0-99 instead of being rejected, allowing unsupported or malformed byte range headers to pass validation.

Suggested fix
+import "strconv"
+
 func ParseByteRange(rangeStr string) (ByteRangeType, error) {
 	if rangeStr == "" {
 		return ByteRangeType{All: true}, nil
 	}
-	// handle open-ended range "N-"
-	if len(rangeStr) > 0 && rangeStr[len(rangeStr)-1] == '-' {
-		var start int64
-		_, err := fmt.Sscanf(rangeStr, "%d-", &start)
+	if strings.HasSuffix(rangeStr, "-") {
+		start, err := strconv.ParseInt(strings.TrimSuffix(rangeStr, "-"), 10, 64)
 		if err != nil || start < 0 {
 			return ByteRangeType{}, errors.New("invalid byte range")
 		}
 		return ByteRangeType{Start: start, OpenEnd: true}, nil
 	}
-	var start, end int64
-	_, err := fmt.Sscanf(rangeStr, "%d-%d", &start, &end)
-	if err != nil {
+	startStr, endStr, ok := strings.Cut(rangeStr, "-")
+	if !ok || startStr == "" || endStr == "" {
 		return ByteRangeType{}, errors.New("invalid byte range")
 	}
+	start, err := strconv.ParseInt(startStr, 10, 64)
+	if err != nil {
+		return ByteRangeType{}, errors.New("invalid byte range")
+	}
+	end, err := strconv.ParseInt(endStr, 10, 64)
+	if err != nil {
+		return ByteRangeType{}, errors.New("invalid byte range")
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/util/fileutil/fileutil.go` around lines 29 - 51, ParseByteRange currently
accepts inputs with trailing garbage because fmt.Sscanf ignores leftover
characters; update parsing to detect leftover input by using the %n verb and
validating that the number of bytes consumed equals len(rangeStr). For the
open-ended case use fmt.Sscanf(rangeStr, "%d-%n", &start, &n) and require n ==
len(rangeStr), and for the closed range use fmt.Sscanf(rangeStr, "%d-%d%n",
&start, &end, &n) and require n == len(rangeStr); keep the existing validation
for negative values and start > end and return errors when n != len(rangeStr) or
Sscanf fails. Ensure these changes are applied inside ParseByteRange and still
return ByteRangeType{Start:..., OpenEnd:true} or ByteRangeType{Start:...,
End:...} as before.

}

func FixPath(path string) (string, error) {
origPath := path
var err error
Expand Down
Loading
Loading