/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
// The Cangjie API is in Beta. For details on its capabilities and limitations, please refer to the README file.
/**
* @file
*
* This file defines Path related classes.
*/
package std.fs
public func canonicalize(path: String): Path {
PathValidator.throwIfEmptyOrContainsNullByte("path", path, true)
if (!exists(path)) {
throw FSException("Failed to canonical: the input path `${path}` may not exist or permission denied!")
}
return Path(processPathHeader(path, Path.canonicalizePath(path)))
}
public func canonicalize(path: Path): Path {
PathValidator.throwIfEmptyOrContainsNullByte("path", path, true)
let rawPath = path.toString()
if (!exists(rawPath)) {
throw FSException("Failed to canonical: the input path `${path}` may not exist or permission denied!")
}
return Path(processPathHeader(rawPath, Path.canonicalizePath(rawPath)))
}
@When[os != "Windows"]
const PATH_SEPARATOR = "/"
@When[os == "Windows"]
const PATH_SEPARATOR = "\\"
@When[os != "Windows"]
const PATH_LISTSEPARATOR = ":"
@When[os == "Windows"]
const PATH_LISTSEPARATOR = ";"
@When[os != "Windows"]
const PATH_SEPARATOR_BYTE = b'/'
@When[os == "Windows"]
const PATH_SEPARATOR_BYTE = b'\\'
@When[os != "Windows"]
const isWindows = false
@When[os == "Windows"]
const isWindows = true
public struct Path <: Equatable<Path> & Hashable & ToString {
let _rawPath: String
public static const Separator: String = PATH_SEPARATOR
public static const ListSeparator: String = PATH_LISTSEPARATOR
public init(rawPath: String) {
_rawPath = rawPath
}
/**
* @throws IllegalArgumentException while path contains null character,
* or path is empty.
*/
public prop parent: Path {
get() {
PathValidator.throwIfEmptyOrContainsNullByte("path", _rawPath, false)
return getParent(_rawPath)
}
}
/**
* @throws IllegalArgumentException while path contains null character,
* or path is empty.
*/
public prop fileName: String {
get() {
PathValidator.throwIfEmptyOrContainsNullByte("path", _rawPath, false)
return getFileName(_rawPath)
}
}
/**
* @throws IllegalArgumentException while path contains null character,
* or path is empty.
*/
public prop extensionName: String {
get() {
PathValidator.throwIfEmptyOrContainsNullByte("path", _rawPath, false)
let name = this.fileName
if (let Some(dot) <- name.lastIndexOf(b'.')) {
return name[dot + 1..]
}
return ""
}
}
/**
* @throws IllegalArgumentException while path contains null character,
* or path is empty.
*/
public prop fileNameWithoutExtension: String {
get() {
PathValidator.throwIfEmptyOrContainsNullByte("path", _rawPath, false)
let name = this.fileName
if (let Some(dot) <- name.lastIndexOf(b'.')) {
return name[..dot]
}
return name
}
}
/**
* @throws IllegalArgumentException while path contains null character,
* or path is empty.
*/
public func isAbsolute(): Bool {
PathValidator.throwIfEmptyOrContainsNullByte("path", _rawPath, false)
return isAbsolutePath(_rawPath)
}
/**
* @throws IllegalArgumentException while path contains null character,
* or path is empty.
*/
public func isRelative(): Bool {
PathValidator.throwIfEmptyOrContainsNullByte("path", _rawPath, false)
return !isAbsolutePath(_rawPath)
}
private func checkWindowsSpecialPrefix(path: String) {
if (!isWindows) {
return
}
let specialPrefix = #"\??\"#
if (path.contains(specialPrefix)) {
throw IllegalArgumentException("Path `${path}` cannot contain `${specialPrefix}`!")
}
}
/**
* @throws IllegalArgumentException while rawpath or path contains null character, or rawpath is empty.
* @throws FSException if path is an absolute path.
*/
public func join(path: String): Path {
PathValidator.throwIfContainsNullByte("path", path, true)
PathValidator.throwIfEmptyOrContainsNullByte("path", _rawPath, false)
if (path.isEmpty()) {
if (isSlash(_rawPath[_rawPath.size - 1])) {
return this
}
return Path(String.join([_rawPath, SLASH_STRING]))
}
if (isAbsolutePath(path)) {
throw FSException("The input path `${path}` cannot be an absolute path!")
}
var newPath = if (isSlash(_rawPath[_rawPath.size - 1])) {
String.join([_rawPath, path])
} else {
String.join([_rawPath, path], delimiter: SLASH_STRING)
}
checkWindowsSpecialPrefix(newPath)
return Path(newPath)
}
/**
* @throws IllegalArgumentException while path contains null character,
* or path is empty.
*/
public func join(path: Path): Path {
return join(path.toString())
}
public func isEmpty(): Bool {
_rawPath.isEmpty()
}
private func checkWindowsMiddleVolumeName(path: String): Bool {
if (!isWindows) {
return false
}
if (let Some(indexOfColon) <- path.indexOf(":")) {
if (indexOfColon <= 1) {
return false
}
if (let Some(indexOfSeparator) <- path.indexOf("/") && indexOfSeparator < indexOfColon) {
return true
}
if (let Some(indexOfSeparator) <- path.indexOf("\\") && indexOfSeparator < indexOfColon) {
return true
}
}
return false
}
private func checkWindowsLeadingVolumeName(path: String): Bool {
if (!isWindows) {
return false
}
if (let Some(indexOfColon) <- path.indexOf(":")) {
if (indexOfColon == 1) {
return true
}
}
return false
}
public func normalize(): Path {
if (_rawPath.isEmpty()) {
return Path(".")
}
// dst string
let newPath = Array<Byte>(_rawPath.size, repeat: 0)
// find root
var rootLen = getRootLen(_rawPath)
// normalize root
for (i in 0..rootLen) {
if (isSlash(_rawPath[i])) {
newPath[i] = PATH_SEPARATOR_BYTE
} else {
newPath[i] = _rawPath[i]
}
}
// only root
if (_rawPath.size == rootLen) {
return unsafe { Path(String.fromUtf8Unchecked(newPath[..rootLen])) }
}
var (left, right, newIdx) = (rootLen, rootLen, rootLen)
// _rawPath[left..right] point to a part between two separators
while (right < _rawPath.size) {
// inside a part, just go on
if (!isSlash(_rawPath[right])) {
right++
continue
}
// end of a part, process the part
newIdx = normalizePart(_rawPath[left..right], newPath, right - left, newIdx, rootLen)
right++
left = right
}
// process the last part
if (left < right) {
newIdx = normalizePart(_rawPath[left..right], newPath, right - left, newIdx, rootLen)
}
// if the result is empty, return "."
if (newIdx == 0) {
return Path(".")
}
let newPathString = unsafe { String.fromUtf8Unchecked(newPath[..newIdx]) }
checkWindowsSpecialPrefix(newPathString)
if (checkWindowsMiddleVolumeName(_rawPath) && checkWindowsLeadingVolumeName(newPathString)) {
return Path(".\\${newPathString}")
}
return Path(newPathString)
}
func normalizePart(part: String, newPath: Array<Byte>, partlen: Int64, newPathIdx: Int64, rootLen: Int64): Int64 {
var newIdx = newPathIdx
return match (part) {
case "" | "." => newPathIdx
case ".." => processDotDot(newPath, newIdx, rootLen)
case _ =>
var newIdx = newPathIdx
// append a separator
if (newIdx != rootLen) {
newPath[newIdx] = PATH_SEPARATOR_BYTE
newIdx++
}
unsafe { part.rawData() }.copyTo(newPath, 0, newIdx, partlen)
newIdx + partlen
}
}
// revert the prev part
// newIdx point to the next position to be written
func processDotDot(newPath: Array<Byte>, newIdx: Int64, rootLen: Int64): Int64 {
if (newIdx == rootLen) {
// if it's first byte after root, and the root ends with a slash, just ignore the ../ part
// if there's a leading slash, it must have been writen to newPath
if (rootLen > 0 && isSlash(newPath[rootLen - 1])) {
return newIdx
} else {
// if it's first byte, and there's no slash before, append .. to newPath
newPath[newIdx] = b'.'
newPath[newIdx + 1] = b'.'
return newIdx + 2
}
}
// find the prev part: newPath[(prev + 1)..newIdx]
var prev = newIdx - 1
var skipPrevSlash = false
while (prev >= rootLen) {
if (isSlash(newPath[prev])) {
skipPrevSlash = true
break
}
prev--
}
// now prev point to the last separator before prev part
// if prev part is "..",append "/.."
if (prev + 3 == newIdx && newPath[prev + 1] == b'.' && newPath[prev + 2] == b'.') {
newPath[newIdx] = PATH_SEPARATOR_BYTE
newPath[newIdx + 1] = b'.'
newPath[newIdx + 2] = b'.'
return newIdx + 3
}
return if (skipPrevSlash) {
prev
} else {
prev + 1
}
}
public operator func ==(other: Path): Bool {
this.normalize()._rawPath == other.normalize()._rawPath
}
public func hashCode(): Int64 {
return this.normalize()._rawPath.hashCode()
}
public func toString(): String {
return _rawPath
}
/**
* @throws FSException if cPath is invalid
*/
static func canonicalizePath(pathStr: String): Array<Byte> {
unsafe {
var realPathArr = Array<Byte>(PATH_MAX_SIZE, repeat: 0)
let cPath = LibC.mallocCString(pathStr)
var arrPtr: CPointerHandle<Byte> = acquireArrayRawData(realPathArr)
let resultptr = CJ_FS_NormalizePath(cPath, arrPtr.pointer)
releaseArrayRawData(arrPtr)
LibC.free(cPath)
if (resultptr.isNull()) {
throw FSException("Failed malloc in C code!")
}
let result = resultptr.read()
LibC.free(resultptr)
let availLen = result.rtnCode
if (availLen <= 0) {
try {
let errMessage = result.msg.toString()
throw FSException("Failed to canonical path `${pathStr}` return ${result.rtnCode}: \"${errMessage.trimAscii()}\".")
} finally {
LibC.free(result.msg)
}
}
realPathArr.slice(0, availLen)
}
}
}
class PathValidator {
private init() {} // static class
static func throwIfEmpty(name: String, path: String, isInput: Bool): Unit {
if (path.isEmpty()) {
throw IllegalArgumentException(generateExceptionMessage(name, "is empty", isInput))
}
}
static func throwIfContainsNullByte(name: String, path: String, isInput: Bool): Unit {
if (path.contains(NULL_BYTE)) {
throw IllegalArgumentException(generateExceptionMessage(name, "value ${unsafe { path.rawData() }} contains null character", isInput))
}
}
static func throwIfEmptyOrContainsNullByte(name: String, path: Path, isInput: Bool): Unit {
throwIfEmptyOrContainsNullByte(name, path.toString(), isInput)
}
static func throwIfEmptyOrContainsNullByte(name: String, path: String, isInput: Bool): Unit {
throwIfEmpty(name, path, isInput)
throwIfContainsNullByte(name, path, isInput)
}
private static func generateExceptionMessage(name: String, postfix: String, isInput: Bool): String {
let sb = StringBuilder(if (isInput) { "The input path " } else { "The path " })
if (name != "path") {
sb.append('\'')
sb.append(name)
sb.append('\'')
sb.append(' ')
}
sb.append(postfix)
sb.append('.')
return sb.toString()
}
}