// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build (js && wasm) || plan9

package os

import (
	"errors"
	"internal/filepathlite"
	"internal/stringslite"
	"sync/atomic"
	"syscall"
	"time"
)

// root implementation for platforms with no openat.
// Currently plan9 and js.
type root struct {
	name   string
	closed atomic.Bool
}

// openRootNolog is OpenRoot.
func openRootNolog(name string) (*Root, error) {
	r, err := newRoot(name)
	if err != nil {
		return nil, &PathError{Op: "open", Path: name, Err: err}
	}
	return r, nil
}

// openRootInRoot is Root.OpenRoot.
func openRootInRoot(r *Root, name string) (*Root, error) {
	if err := checkPathEscapes(r, name); err != nil {
		return nil, &PathError{Op: "openat", Path: name, Err: err}
	}
	r, err := newRoot(joinPath(r.root.name, name))
	if err != nil {
		return nil, &PathError{Op: "openat", Path: name, Err: err}
	}
	return r, nil
}

// newRoot returns a new Root.
// If fd is not a directory, it closes it and returns an error.
func newRoot(name string) (*Root, error) {
	fi, err := Stat(name)
	if err != nil {
		return nil, err.(*PathError).Err
	}
	if !fi.IsDir() {
		return nil, errors.New("not a directory")
	}
	return &Root{&root{name: name}}, nil
}

func (r *root) Close() error {
	// For consistency with platforms where Root.Close closes a handle,
	// mark the Root as closed and return errors from future calls.
	r.closed.Store(true)
	return nil
}

func (r *root) Name() string {
	return r.name
}

// rootOpenFileNolog is Root.OpenFile.
func rootOpenFileNolog(r *Root, name string, flag int, perm FileMode) (*File, error) {
	if err := checkPathEscapes(r, name); err != nil {
		return nil, &PathError{Op: "openat", Path: name, Err: err}
	}
	f, err := openFileNolog(joinPath(r.root.name, name), flag, perm)
	if err != nil {
		return nil, &PathError{Op: "openat", Path: name, Err: underlyingError(err)}
	}
	return f, nil
}

func rootStat(r *Root, name string, lstat bool) (FileInfo, error) {
	var fi FileInfo
	var err error
	if lstat {
		err = checkPathEscapesLstat(r, name)
		if err == nil {
			fi, err = Lstat(joinPath(r.root.name, name))
		}
	} else {
		err = checkPathEscapes(r, name)
		if err == nil {
			fi, err = Stat(joinPath(r.root.name, name))
		}
	}
	if err != nil {
		return nil, &PathError{Op: "statat", Path: name, Err: underlyingError(err)}
	}
	return fi, nil
}

func rootChmod(r *Root, name string, mode FileMode) error {
	if err := checkPathEscapes(r, name); err != nil {
		return &PathError{Op: "chmodat", Path: name, Err: err}
	}
	if err := Chmod(joinPath(r.root.name, name), mode); err != nil {
		return &PathError{Op: "chmodat", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootChown(r *Root, name string, uid, gid int) error {
	if err := checkPathEscapes(r, name); err != nil {
		return &PathError{Op: "chownat", Path: name, Err: err}
	}
	if err := Chown(joinPath(r.root.name, name), uid, gid); err != nil {
		return &PathError{Op: "chownat", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootLchown(r *Root, name string, uid, gid int) error {
	if err := checkPathEscapesLstat(r, name); err != nil {
		return &PathError{Op: "lchownat", Path: name, Err: err}
	}
	if err := Lchown(joinPath(r.root.name, name), uid, gid); err != nil {
		return &PathError{Op: "lchownat", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
	if err := checkPathEscapes(r, name); err != nil {
		return &PathError{Op: "chtimesat", Path: name, Err: err}
	}
	if err := Chtimes(joinPath(r.root.name, name), atime, mtime); err != nil {
		return &PathError{Op: "chtimesat", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootMkdir(r *Root, name string, perm FileMode) error {
	if err := checkPathEscapes(r, name); err != nil {
		return &PathError{Op: "mkdirat", Path: name, Err: err}
	}
	if name == "" {
		return &PathError{Op: "mkdirat", Path: name, Err: syscall.ENOENT}
	}
	if err := Mkdir(joinPath(r.root.name, name), perm); err != nil {
		return &PathError{Op: "mkdirat", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootMkdirAll(r *Root, name string, perm FileMode) error {
	// We only check for errPathEscapes here.
	// For errors such as ENOTDIR (a non-directory file appeared somewhere along the path),
	// we let MkdirAll generate the error.
	// MkdirAll will return a PathError referencing the exact location of the error,
	// and we want to preserve that property.
	if err := checkPathEscapes(r, name); err == errPathEscapes {
		return &PathError{Op: "mkdirat", Path: name, Err: err}
	}
	if name == "" {
		return &PathError{Op: "mkdirat", Path: name, Err: syscall.ENOENT}
	}
	prefix := r.root.name + string(PathSeparator)
	if err := MkdirAll(prefix+name, perm); err != nil {
		if pe, ok := err.(*PathError); ok {
			pe.Op = "mkdirat"
			pe.Path = stringslite.TrimPrefix(pe.Path, prefix)
			return pe
		}
		return &PathError{Op: "mkdirat", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootRemove(r *Root, name string) error {
	if err := checkPathEscapesLstat(r, name); err != nil {
		return &PathError{Op: "removeat", Path: name, Err: err}
	}
	if endsWithDot(name) {
		// We don't want to permit removing the root itself, so check for that.
		if filepathlite.Clean(name) == "." {
			return &PathError{Op: "removeat", Path: name, Err: errPathEscapes}
		}
	}
	if err := Remove(joinPath(r.root.name, name)); err != nil {
		return &PathError{Op: "removeat", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootRemoveAll(r *Root, name string) error {
	// Consistency with os.RemoveAll: Strip trailing /s from the name,
	// so RemoveAll("not_a_directory/") succeeds.
	for len(name) > 0 && IsPathSeparator(name[len(name)-1]) {
		name = name[:len(name)-1]
	}
	if endsWithDot(name) {
		// Consistency with os.RemoveAll: Return EINVAL when trying to remove .
		return &PathError{Op: "RemoveAll", Path: name, Err: syscall.EINVAL}
	}
	if err := checkPathEscapesLstat(r, name); err != nil {
		if err == syscall.ENOTDIR {
			// Some intermediate path component is not a directory.
			// RemoveAll treats this as success (since the target doesn't exist).
			return nil
		}
		return &PathError{Op: "RemoveAll", Path: name, Err: err}
	}
	if err := RemoveAll(joinPath(r.root.name, name)); err != nil {
		return &PathError{Op: "RemoveAll", Path: name, Err: underlyingError(err)}
	}
	return nil
}

func rootReadlink(r *Root, name string) (string, error) {
	if err := checkPathEscapesLstat(r, name); err != nil {
		return "", &PathError{Op: "readlinkat", Path: name, Err: err}
	}
	name, err := Readlink(joinPath(r.root.name, name))
	if err != nil {
		return "", &PathError{Op: "readlinkat", Path: name, Err: underlyingError(err)}
	}
	return name, nil
}

func rootRename(r *Root, oldname, newname string) error {
	if err := checkPathEscapesLstat(r, oldname); err != nil {
		return &PathError{Op: "renameat", Path: oldname, Err: err}
	}
	if err := checkPathEscapesLstat(r, newname); err != nil {
		return &PathError{Op: "renameat", Path: newname, Err: err}
	}
	err := Rename(joinPath(r.root.name, oldname), joinPath(r.root.name, newname))
	if err != nil {
		return &LinkError{"renameat", oldname, newname, underlyingError(err)}
	}
	return nil
}

func rootLink(r *Root, oldname, newname string) error {
	if err := checkPathEscapesLstat(r, oldname); err != nil {
		return &PathError{Op: "linkat", Path: oldname, Err: err}
	}
	fullOldName := joinPath(r.root.name, oldname)
	if fs, err := Lstat(fullOldName); err == nil && fs.Mode()&ModeSymlink != 0 {
		return &PathError{Op: "linkat", Path: oldname, Err: errors.New("cannot create a hard link to a symlink")}
	}
	if err := checkPathEscapesLstat(r, newname); err != nil {
		return &PathError{Op: "linkat", Path: newname, Err: err}
	}
	err := Link(fullOldName, joinPath(r.root.name, newname))
	if err != nil {
		return &LinkError{"linkat", oldname, newname, underlyingError(err)}
	}
	return nil
}

func rootSymlink(r *Root, oldname, newname string) error {
	if err := checkPathEscapesLstat(r, newname); err != nil {
		return &PathError{Op: "symlinkat", Path: newname, Err: err}
	}
	err := Symlink(oldname, joinPath(r.root.name, newname))
	if err != nil {
		return &LinkError{"symlinkat", oldname, newname, underlyingError(err)}
	}
	return nil
}
