mirror of
https://github.com/wavetermdev/waveterm
synced 2026-04-30 19:07:17 +00:00
239 lines
5.1 KiB
Go
239 lines
5.1 KiB
Go
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package fileutil
|
|
|
|
import (
|
|
"io"
|
|
"io/fs"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
|
)
|
|
|
|
func FixPath(path string) (string, error) {
|
|
var err error
|
|
if strings.HasPrefix(path, "~") {
|
|
path = filepath.Join(wavebase.GetHomeDir(), path[1:])
|
|
} else if !filepath.IsAbs(path) {
|
|
path, err = filepath.Abs(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
const (
|
|
winFlagSoftlink = uint32(0x8000) // FILE_ATTRIBUTE_REPARSE_POINT
|
|
winFlagJunction = uint32(0x80) // FILE_ATTRIBUTE_JUNCTION
|
|
)
|
|
|
|
func WinSymlinkDir(path string, bits os.FileMode) bool {
|
|
// Windows compatibility layer doesn't expose symlink target type through fileInfo
|
|
// so we need to check file attributes and extension patterns
|
|
isFileSymlink := func(filepath string) bool {
|
|
if len(filepath) == 0 {
|
|
return false
|
|
}
|
|
return strings.LastIndex(filepath, ".") > strings.LastIndex(filepath, "/")
|
|
}
|
|
|
|
flags := uint32(bits >> 12)
|
|
|
|
if flags == winFlagSoftlink {
|
|
return !isFileSymlink(path)
|
|
} else if flags == winFlagJunction {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// on error just returns ""
|
|
// does not return "application/octet-stream" as this is considered a detection failure
|
|
// can pass an existing fileInfo to avoid re-statting the file
|
|
// falls back to text/plain for 0 byte files
|
|
|
|
func DetectMimeType(path string, fileInfo fs.FileInfo, extended bool) string {
|
|
if fileInfo == nil {
|
|
statRtn, err := os.Stat(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
fileInfo = statRtn
|
|
}
|
|
|
|
if fileInfo.IsDir() || WinSymlinkDir(path, fileInfo.Mode()) {
|
|
return "directory"
|
|
}
|
|
if fileInfo.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
|
|
return "pipe"
|
|
}
|
|
charDevice := os.ModeDevice | os.ModeCharDevice
|
|
if fileInfo.Mode()&charDevice == charDevice {
|
|
return "character-special"
|
|
}
|
|
if fileInfo.Mode()&os.ModeDevice == os.ModeDevice {
|
|
return "block-special"
|
|
}
|
|
ext := filepath.Ext(path)
|
|
if mimeType, ok := StaticMimeTypeMap[ext]; ok {
|
|
return mimeType
|
|
}
|
|
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
|
return mimeType
|
|
}
|
|
if fileInfo.Size() == 0 {
|
|
return "text/plain"
|
|
}
|
|
if !extended {
|
|
return ""
|
|
}
|
|
fd, err := os.Open(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer fd.Close()
|
|
buf := make([]byte, 512)
|
|
// ignore the error (EOF / UnexpectedEOF is fine, just process how much we got back)
|
|
n, _ := io.ReadAtLeast(fd, buf, 512)
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
buf = buf[:n]
|
|
rtn := http.DetectContentType(buf)
|
|
if rtn == "application/octet-stream" {
|
|
return ""
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func DetectMimeTypeWithDirEnt(path string, dirEnt fs.DirEntry) string {
|
|
if dirEnt != nil {
|
|
if dirEnt.IsDir() {
|
|
return "directory"
|
|
}
|
|
mode := dirEnt.Type()
|
|
if mode&os.ModeNamedPipe == os.ModeNamedPipe {
|
|
return "pipe"
|
|
}
|
|
charDevice := os.ModeDevice | os.ModeCharDevice
|
|
if mode&charDevice == charDevice {
|
|
return "character-special"
|
|
}
|
|
if mode&os.ModeDevice == os.ModeDevice {
|
|
return "block-special"
|
|
}
|
|
}
|
|
ext := filepath.Ext(path)
|
|
if mimeType, ok := StaticMimeTypeMap[ext]; ok {
|
|
return mimeType
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var (
|
|
systemBinDirs = []string{
|
|
"/bin/",
|
|
"/usr/bin/",
|
|
"/usr/local/bin/",
|
|
"/opt/bin/",
|
|
"/sbin/",
|
|
"/usr/sbin/",
|
|
}
|
|
suspiciousPattern = regexp.MustCompile(`[:;#!&$\t%="|>{}]`)
|
|
flagPattern = regexp.MustCompile(` --?[a-zA-Z0-9]`)
|
|
)
|
|
|
|
// IsInitScriptPath tries to determine if the input string is a path to a script
|
|
// rather than an inline script content.
|
|
func IsInitScriptPath(input string) bool {
|
|
if len(input) == 0 || strings.Contains(input, "\n") {
|
|
return false
|
|
}
|
|
|
|
if suspiciousPattern.MatchString(input) {
|
|
return false
|
|
}
|
|
|
|
if flagPattern.MatchString(input) {
|
|
return false
|
|
}
|
|
|
|
// Check for home directory path
|
|
if strings.HasPrefix(input, "~/") {
|
|
return true
|
|
}
|
|
|
|
// Path must be absolute (if not home directory)
|
|
if !filepath.IsAbs(input) {
|
|
return false
|
|
}
|
|
|
|
// Check if path starts with system binary directories
|
|
normalizedPath := filepath.ToSlash(input)
|
|
for _, binDir := range systemBinDirs {
|
|
if strings.HasPrefix(normalizedPath, binDir) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type FsFileInfo struct {
|
|
NameInternal string
|
|
ModeInternal os.FileMode
|
|
SizeInternal int64
|
|
ModTimeInternal int64
|
|
IsDirInternal bool
|
|
}
|
|
|
|
func (f FsFileInfo) Name() string {
|
|
return f.NameInternal
|
|
}
|
|
|
|
func (f FsFileInfo) Size() int64 {
|
|
return f.SizeInternal
|
|
}
|
|
|
|
func (f FsFileInfo) Mode() os.FileMode {
|
|
return f.ModeInternal
|
|
}
|
|
|
|
func (f FsFileInfo) ModTime() time.Time {
|
|
return time.Unix(0, f.ModTimeInternal)
|
|
}
|
|
|
|
func (f FsFileInfo) IsDir() bool {
|
|
return f.IsDirInternal
|
|
}
|
|
|
|
func (f FsFileInfo) Sys() interface{} {
|
|
return nil
|
|
}
|
|
|
|
var _ fs.FileInfo = FsFileInfo{}
|
|
|
|
// ToFsFileInfo converts wshrpc.FileInfo to FsFileInfo.
|
|
// It panics if fi is nil.
|
|
func ToFsFileInfo(fi *wshrpc.FileInfo) FsFileInfo {
|
|
if fi == nil {
|
|
panic("ToFsFileInfo: nil FileInfo")
|
|
}
|
|
return FsFileInfo{
|
|
NameInternal: fi.Name,
|
|
ModeInternal: fi.Mode,
|
|
SizeInternal: fi.Size,
|
|
ModTimeInternal: fi.ModTime,
|
|
IsDirInternal: fi.IsDir,
|
|
}
|
|
}
|