/*
* 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 is a library for File.
*/
package std.fs
import std.io.*
const SEEK_SET: Int32 = 0 /* Seek from beginning of file. */
const SEEK_CUR: Int32 = 1 /* Seek from current position. */
const SEEK_END: Int32 = 2 /* Seek from end of file. */
const READ_ONLY: Int32 = 0
const WRITE_ONLY: Int32 = 1
const APPEND: Int32 = 2
const READ_WRITE: Int32 = 3
public enum OpenMode <: ToString & Equatable<OpenMode> {
| Read // O_RDONLY
| Write // O_WRONLY | O_CREAT | O_TRUNC
| Append // O_WRONLY | O_CREAT | O_APPEND
| ReadWrite // O_RDWR | O_CREAT
public operator func ==(other: OpenMode): Bool {
return match ((this, other)) {
case (Read, Read) => true
case (Write, Write) => true
case (Append, Append) => true
case (ReadWrite, ReadWrite) => true
case _ => false
}
}
public operator func !=(other: OpenMode): Bool {
return !(this == other)
}
public func toString(): String {
return match (this) {
case Read => "Read"
case Write => "Write"
case Append => "Append"
case ReadWrite => "ReadWrite"
}
}
}
// FD numbers are indexes into the FD table.
// Unix/Linux use the Int type for handle, while Windows use HANDLE type.
// HANDLE is typedef'd void *, which is really just a 32-bit index, only for more opaque.
public struct FileDescriptor {
var _fileHandle: IntNative = InvalidHandle
public prop fileHandle: IntNative {
get() {
_fileHandle
}
}
}
public class File <: Resource & IOStream & Seekable {
private var _fileInfo: FileInfo
private var _openMode: OpenMode
var _fileDescriptor: FileDescriptor = FileDescriptor()
private var _canRead: Bool = true
private var _canWrite: Bool = true
private var _canSeek: Bool = true
~init() {
if (isHandleValid(_fileDescriptor._fileHandle)) {
unsafe { CJ_FS_CloseFile(_fileDescriptor._fileHandle) }
_fileDescriptor._fileHandle = InvalidHandle
}
}
/**
* Constructors
*
* @param path - The file path.
* @param mode - The open mode.
*
* @throws IllegalArgumentException - If path is empty or path contains null character.
* @throws FSException - If the file does not exist while operation is open,
* the file already exists while operation is create,
* the parent directory of the file does not exist,
* or other reasons caused fail to open file.
*/
public init(path: String, mode: OpenMode) {
PathValidator.throwIfEmptyOrContainsNullByte("path", path, true)
let filePath: Path = Path(path)
let parent = filePath.parent
if (!parent.isEmpty() && !exists(parent)) {
throw FSException("The input path 'parent' `${parent}` does not exist.")
}
// Because `filePath` may not exist before `openFile` is executed, `FileInfo` does not check whether the path exists.
_fileInfo = FileInfo(filePath, false)
_openMode = mode
openFile()
}
/**
* Constructors
*
* @param path - The file path.
* @param mode - The open mode.
*
* @throws IllegalArgumentException - If path is empty or contains null character.
* @throws FSException - If the file does not exist while operation is open,
* the file already exists while operation is create,
* the parent directory of the file does not exist,
* or other reasons caused fail to open file.
*/
public init(path: Path, mode: OpenMode) {
this(path.toString(), mode)
}
private init(path: String, mode: OpenMode, fd: FileHandle) {
if (!isHandleValid(fd)) {
throw FSException("Invalid File Handle ${fd}.")
}
let filePath: Path = Path(path)
_fileInfo = FileInfo(filePath, false)
_openMode = mode
fileHandle = fd
}
public prop info: FileInfo {
get() {
_fileInfo
}
}
public prop fileDescriptor: FileDescriptor {
get() {
_fileDescriptor
}
}
mut prop fileHandle: FileHandle {
get() {
_fileDescriptor._fileHandle
}
set(v) {
_fileDescriptor._fileHandle = v
}
}
public prop length: Int64 {
get() {
getsize()
}
}
public func setLength(length: Int64): Unit {
if (length < 0) {
throw IllegalArgumentException("Invalid length: ${length}.")
}
checkCanTruncate()
unsafe {
let resultPtr = CJ_FS_Truncate(_fileDescriptor.fileHandle, length)
if (resultPtr.isNull()) {
throw FSException("Failed to tuncate the file `${_fileInfo.path}` return null.")
}
let result = resultPtr.read()
LibC.free(resultPtr)
try {
if (result.rtnCode != 0) {
throw FSException("Failed to tuncate the file `${_fileInfo.path}`: ${result.msg}.")
}
} finally {
LibC.free(result.msg) // it's safe to free(NULL)
}
}
}
/**
* Read data from file.
*
* @param buffer - Read data from file to the byte array.
*
* @return Int64 - Size of the data be read.
*
* @throws IllegalArgumentException - If buffer is empty.
* @throws FSException - If the file to read is not opened,
* or the file does not have the read permission
* or failed to read file
*/
public func read(buffer: Array<Byte>): Int64 {
checkCanRead()
if (buffer.size == 0) {
throw IllegalArgumentException("The buffer is empty.")
}
var readBytes: Int64 = directRead(buffer)
if (readBytes == 0) {
return 0
} else if (readBytes < 0) {
throw FSException("The file `${_fileInfo.path}` read error return ${readBytes} bytes.")
}
return readBytes
}
/**
* @throws FSException if system failed to write the file
* @throws FSException if file is not opened
* @throws FSException if the file is not allowed to write
*/
public func write(buffer: Array<Byte>): Unit {
if (!isHandleValid(fileHandle)) {
throw FSException("The file `${_fileInfo.path}` not opened, can not be written.")
}
if (!_canWrite) {
throw FSException("The file `${_fileInfo.path}` does not have the write permission.")
}
if (buffer.size == 0) {
return
}
directWrite(buffer)
}
public func flush(): Unit {}
/**
* @throws FSException if file can not seek
* @throws FSException if there is error in parameter
* @throws FSException if unknown errors occurred
*/
public func seek(sp: SeekPosition): Int64 {
if (!canSeek()) {
throw FSException("The file `${_fileInfo.path}` can not seek.")
}
var (whence, offset): (Int32, Int64) = match (sp) {
case Current(cOffset) => (SEEK_CUR, cOffset)
case Begin(bOffset) => (SEEK_SET, bOffset)
case End(eOffset) => (SEEK_END, eOffset)
}
var val: Int64 = unsafe { CJ_FS_Seek(fileHandle, whence, offset) }
if (val < 0) {
throw FSException("Failed to seek file `${_fileInfo.path}`: errno is ${0 - val}.")
}
return val
}
private func canSeek(): Bool {
if (isClosed()) {
return false
}
return _canSeek
}
public func canRead(): Bool {
if (isClosed()) {
return false
}
return _canRead
}
public func canWrite(): Bool {
if (isClosed()) {
return false
}
return _canWrite
}
/**
* @throws FSException if system failed to close file
*/
public func close(): Unit {
if (!isHandleValid(fileHandle)) {
return
}
flush()
/* Call the close() in the C Language */
if (unsafe { CJ_FS_CloseFile(fileHandle) } < 0) {
throw FSException("Failed to close file `${_fileInfo.path}`.")
}
fileHandle = InvalidHandle
}
public func isClosed(): Bool {
return !isHandleValid(fileHandle)
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
*/
public static func create(path: Path): File {
return create(path.toString())
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
*/
public static func create(path: String): File {
if (exists(path)) {
throw FSException("The file `${path}` is already exists.")
}
return File(path, Write)
}
/**
* @throws FSException if failed to create the temporary file or directoryPath is invalid.
* @throws IllegalArgumentException while path contains null character or path is empty.
*/
public static func createTemp(directoryPath: String): File {
return createTemp(Path(directoryPath))
}
/**
* @throws FSException if failed to create the temporary file or directoryPath is invalid.
* @throws IllegalArgumentException while path contains null character or path is empty.
*/
public static func createTemp(directoryPath: Path): File {
var utf8View = unsafe { canonicalize(directoryPath).toString().rawData() }
// Append r'/' + "tmpfile" + Six random characters and r'\0'
var appendStrUtf8View = TMP_FILE
var tempArrSize = utf8View.size + appendStrUtf8View.size
var tempArr = Array<Byte>(tempArrSize, repeat: 0)
utf8View.copyTo(tempArr, 0, 0, utf8View.size)
appendStrUtf8View.copyTo(tempArr, 0, utf8View.size, appendStrUtf8View.size)
var ret: FileHandle = InvalidHandle
unsafe {
var arrPtr: CPointerHandle<Byte> = acquireArrayRawData(tempArr)
ret = CJ_FS_CreateTempFile(arrPtr.pointer)
releaseArrayRawData(arrPtr)
if (!isHandleValid(ret)) {
throw FSException("Failed to create the temporary file!")
}
}
var tempPath = String.fromUtf8(tempArr.slice(0, tempArrSize - 1))
return File(tempPath, ReadWrite, ret)
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
*/
static func copy(sourcePath: String, destinationPath: String, overwrite: Bool): Unit {
if (overwrite) {
callNativeFunc(sourcePath, destinationPath, CJ_FS_CopyREF,
"Failed to copy file from `${sourcePath}` to `${destinationPath}`")
return
}
try (srcFile = File(sourcePath, Read), dstFile = File(destinationPath, Write)) {
copy(srcFile, to: dstFile)
}
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
*/
public static func readFrom(path: Path): Array<Byte> {
File.readFrom(path.toString())
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
* @throws FSException if file read failed
* @throws FSException if system failed to close file
*/
public static func readFrom(path: String): Array<Byte> {
var file = File(path, Read)
try {
return readToEnd(file)
} finally {
file.close()
}
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
* @throws FSException if system failed to close file.
*/
public static func writeTo(path: Path, buffer: Array<Byte>): Unit {
writeTo(path.toString(), buffer)
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
* @throws FSException if system failed to close file.
*/
public static func writeTo(path: String, buffer: Array<Byte>): Unit {
writeTo(path, buffer, Write)
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
* @throws FSException if system failed to close file
*/
public static func appendTo(path: Path, buffer: Array<Byte>): Unit {
appendTo(path.toString(), buffer)
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
* @throws FSException if system failed to close file
*/
public static func appendTo(path: String, buffer: Array<Byte>): Unit {
writeTo(path, buffer, OpenMode.Append)
}
/**
* @throws FSException while path is empty.
* @throws IllegalArgumentException while path contains null character.
* @throws FSException if system failed to close file
*/
private static func writeTo(path: String, buffer: Array<Byte>, mode: OpenMode): Unit {
var file = File(path, mode)
try {
file.write(buffer)
} finally {
file.close()
}
}
/*
* private functions
*/
/**
* Open file according to options.
*
* @throws FSException if check parameter is invalid or failed to get offset
* @throws FSException if failed to open file
* @throws FSException if file does not exist when operation is open
* @throws FSException if file already exists when operation is create
* @throws FSException if path is invalid
*/
private func openFile(): Unit {
let isExists = exists(_fileInfo.path)
let (r, w, s, openMode): (Bool, Bool, Bool, Int32) = match ((_openMode, isExists)) {
case (Read, false) => throw FSException("The file `${_fileInfo.path}` does not exist or permission denied!")
case (Read, _) => (true, false, true, READ_ONLY)
case (Write, _) => (false, true, true, WRITE_ONLY)
case (OpenMode.Append, _) => (false, true, false, APPEND)
case (ReadWrite, _) => (true, true, true, READ_WRITE)
}
_canRead = r
_canWrite = w
_canSeek = s
var fsInfoCp: CPointer<FsInfo>
var fsInfo: FsInfo
var fsInfoErrMsg = ""
let pathStr = _fileInfo.path.toString()
unsafe {
let cPath = LibC.mallocCString(pathStr)
fsInfoCp = CJ_FS_OpenFile(cPath, openMode)
LibC.free(cPath)
if (fsInfoCp.isNull()) {
throw FSException("Failed to open the file `${pathStr}`.")
}
fsInfo = fsInfoCp.read()
LibC.free(fsInfoCp)
try {
fsInfoErrMsg = fsInfo.msg.toString()
} finally {
LibC.free(fsInfo.msg)
}
_fileDescriptor._fileHandle = fsInfo.fd
}
if (!isHandleValid(_fileDescriptor._fileHandle)) {
var errMsg = "Failed to open the file `${pathStr}`."
if (fsInfoErrMsg != "") {
errMsg += " ${fsInfoErrMsg}."
}
throw FSException(errMsg)
}
}
private func getsize(): Int64 {
if (!isHandleValid(_fileDescriptor._fileHandle)) {
return -1
}
unsafe {
return CJ_FS_GetFileSize(_fileDescriptor._fileHandle)
}
}
/**
* @throws FSException if the file to read is not opened
* @throws FSException if the file does not have the read permission
*/
private func checkCanRead(): Unit {
if (!isHandleValid(_fileDescriptor._fileHandle)) {
throw FSException("The file `${_fileInfo.path}` not opened, can not to read.")
}
if (!_canRead) {
throw FSException("The file `${_fileInfo.path}` does not have the read permission.")
}
}
/**
* @throws FSException if the file to read is not opened
* @throws FSException if the file does not have the read permission
*/
private func checkCanTruncate(): Unit {
if (!isHandleValid(_fileDescriptor._fileHandle)) {
throw FSException("The file `${_fileInfo.path}` not opened, can not to read.")
}
match (_openMode) {
case Write | ReadWrite => ()
case _ => throw FSException("The file `${_fileInfo.path}` does not have the write permission.")
}
}
/**
* Read from the file directly and write into buffer.
*
* @param buffer - Read to the buffer from the file
*
* @return Length successfully written into the array buffer
* @since 0.17.4
*/
private func directRead(buffer: Array<Byte>): Int64 {
unsafe {
let bufPtr: CPointerHandle<Byte> = acquireArrayRawData(buffer)
let readSize: Int64 = CJ_FS_FileRead(fileHandle, bufPtr.pointer, UIntNative(buffer.size))
releaseArrayRawData(bufPtr)
return readSize
}
}
/**
* Write the array to the file and return the bytes successfully written into the file.
*
* @param buffer – The buffer will be written to the file
*
* @since 0.17.4
*
* @throws FSException if system failed to write the file
*/
private func directWrite(buffer: Array<Byte>): Unit {
unsafe {
let bufSize: Int64 = buffer.size
var bufPtr: CPointerHandle<Byte> = acquireArrayRawData(buffer)
let writeSuccess: Bool = CJ_FS_FileWrite(fileHandle, bufPtr.pointer, UIntNative(bufSize))
releaseArrayRawData(bufPtr)
if (!writeSuccess) {
throw FSException("The file `${_fileInfo.path}` write error.")
}
}
}
}