├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .releaserc.yml ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── gleam.toml ├── manifest.toml ├── src ├── file_streams │ ├── file_open_mode.gleam │ ├── file_stream.gleam │ ├── file_stream_error.gleam │ ├── internal │ │ ├── raw_location.gleam │ │ ├── raw_read_result.gleam │ │ └── raw_result.gleam │ └── text_encoding.gleam ├── file_streams_ffi.erl └── file_streams_ffi.mjs └── test ├── file_stream_error_test.gleam └── file_streams_test.gleam /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-24.04 15 | environment: release 16 | timeout-minutes: 10 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup BEAM 23 | uses: erlef/setup-beam@v1 24 | with: 25 | otp-version: 28.0 26 | gleam-version: 1.11.0 27 | 28 | - name: Wait for tests to complete 29 | uses: lewagon/wait-on-check-action@v1.3.4 30 | with: 31 | ref: ${{ github.sha }} 32 | repo-token: ${{ secrets.GITHUB_TOKEN }} 33 | check-regexp: Test 34 | 35 | - name: Semantic release 36 | id: semantic-release 37 | uses: cycjimmy/semantic-release-action@v4 38 | with: 39 | extra_plugins: conventional-changelog-conventionalcommits 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Publish to Hex package manager 44 | if: steps.semantic-release.outputs.new_release_published == 'true' 45 | env: 46 | HEXPM_API_KEY: ${{ secrets.HEXPM_API_KEY }} 47 | run: | 48 | echo 'version = "${{ steps.semantic-release.outputs.new_release_version }}"' | cat - gleam.toml > gleam.toml.new 49 | mv gleam.toml.new gleam.toml 50 | gleam publish --yes 51 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup BEAM 18 | uses: erlef/setup-beam@v1 19 | with: 20 | otp-version: 28.0 21 | gleam-version: 1.11.0 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 22.16 27 | 28 | - name: Setup Deno 29 | uses: denoland/setup-deno@v2 30 | with: 31 | deno-version: 2.3 32 | 33 | - name: Setup Bun 34 | uses: oven-sh/setup-bun@v2 35 | with: 36 | bun-version: 1.2 37 | 38 | - name: Install dependencies 39 | run: gleam deps download 40 | 41 | - name: Run tests 42 | run: | 43 | gleam test --target erlang 44 | gleam test --target javascript --runtime node 45 | gleam test --target javascript --runtime deno 46 | gleam test --target javascript --runtime bun 47 | 48 | - name: Check code formatting 49 | run: gleam format --check src test 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.beam 3 | *.ez 4 | /build 5 | erl_crash.dump 6 | /*.test 7 | /test.txt 8 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | preset: conventionalcommits 2 | 3 | plugins: 4 | - "@semantic-release/commit-analyzer" 5 | - "@semantic-release/release-notes-generator" 6 | - "@semantic-release/github" 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [80], 3 | 4 | "files.exclude": { 5 | "**/build": true, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Richard Viney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gleam File Streams 2 | 3 | This Gleam library provides access to file streams for reading and writing 4 | files. If you don't require streaming behavior then consider using 5 | [`simplifile`](https://hex.pm/packages/simplifile) instead. 6 | 7 | [![Package Version](https://img.shields.io/hexpm/v/file_streams)](https://hex.pm/packages/file_streams) 8 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/file_streams/) 9 | ![Erlang Compatible](https://img.shields.io/badge/target-erlang-a90432) 10 | ![JavaScript Compatible](https://img.shields.io/badge/target-javascript-f3e155) 11 | [![Semantic Release](https://img.shields.io/badge/semantic--release-conventionalcommits-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) 12 | 13 | ## Usage 14 | 15 | Add this library to your project: 16 | 17 | ```sh 18 | gleam add file_streams 19 | ``` 20 | 21 | The following code writes data to a file using a file stream, then reads it back 22 | in using a second file stream, first as raw bytes and then as lines of UTF-8 23 | text. 24 | 25 | ```gleam 26 | import file_streams/file_stream 27 | import file_streams/file_stream_error 28 | 29 | pub fn main() { 30 | let filename = "test.txt" 31 | 32 | // Write file 33 | let assert Ok(stream) = file_stream.open_write(filename) 34 | let assert Ok(Nil) = file_stream.write_bytes(stream, <<"Hello!\n":utf8>>) 35 | let assert Ok(Nil) = file_stream.write_chars(stream, "12") 36 | let assert Ok(Nil) = file_stream.close(stream) 37 | 38 | // Read file 39 | let assert Ok(stream) = file_stream.open_read(filename) 40 | let assert Ok(<<"Hello!\n":utf8>>) = file_stream.read_bytes(stream, 7) 41 | let assert Ok([49, 50]) = 42 | file_stream.read_list(stream, file_stream.read_uint8, 2) 43 | let assert Error(file_stream_error.Eof) = file_stream.read_bytes(stream, 1) 44 | 45 | // Reset file position to the start and read line by line (not currently 46 | // supported on JavaScript) 47 | let assert Ok(0) = 48 | file_stream.position(stream, file_stream.BeginningOfFile(0)) 49 | let assert Ok("Hello!\n") = file_stream.read_line(stream) 50 | let assert Ok("12") = file_stream.read_line(stream) 51 | let assert Error(file_stream_error.Eof) = file_stream.read_line(stream) 52 | let assert Ok(Nil) = file_stream.close(stream) 53 | } 54 | ``` 55 | 56 | ### Working with Text Encodings 57 | 58 | > [!NOTE] 59 | > Text encodings are not currently supported on the JavaScript target. 60 | 61 | If a text encoding is specified when opening a file stream it allows for 62 | reading and writing of characters and lines of text stored in that encoding. 63 | To open a text file stream use the `file_stream.open_read_text()` and 64 | `file_stream.open_write_text()` functions. The supported encodings are `Latin1`, 65 | `Unicode` (UTF-8), `Utf16`, and `Utf32`. The default encoding is `Latin1`. 66 | 67 | File streams opened with a text encoding aren't compatible with the `Raw` file 68 | open mode that significantly improves IO performance on Erlang. Specifying both 69 | `Raw` and `Encoding` when calling `file_stream.open()` returns `Error(Enotsup)`. 70 | 71 | Although a text encoding can't be specified with `Raw` mode, the 72 | `file_stream.read_line()` and `file_stream.write_chars()` functions can still be 73 | used to work with UTF-8 data. This means that text encoded as UTF-8 can be 74 | handled with high performance in `Raw` mode. 75 | 76 | When a text encoding other than `Latin1` is specified, functions that read and 77 | write raw bytes and other binary data aren't supported and will return 78 | `Error(Enotsup)`. 79 | 80 | The following code demonstrates working with a UTF-16 file stream. 81 | 82 | ```gleam 83 | import file_streams/file_stream 84 | import file_streams/file_stream_error 85 | import file_streams/text_encoding 86 | 87 | pub fn main() { 88 | let filename = "test.txt" 89 | let encoding = text_encoding.Utf16(text_encoding.Little) 90 | 91 | // Write UTF-16 text file 92 | let assert Ok(stream) = file_stream.open_write_text(filename, encoding) 93 | let assert Ok(Nil) = file_stream.write_chars(stream, "Hello!\n") 94 | let assert Ok(Nil) = file_stream.write_chars(stream, "Gleam is cool!\n") 95 | let assert Ok(Nil) = file_stream.close(stream) 96 | 97 | // Read UTF-16 text file 98 | let assert Ok(stream) = file_stream.open_read_text(filename, encoding) 99 | let assert Ok("Hello!\n") = file_stream.read_line(stream) 100 | let assert Ok("Gleam") = file_stream.read_chars(stream, 5) 101 | let assert Ok(" is cool!\n") = file_stream.read_line(stream) 102 | let assert Error(file_stream_error.Eof) = file_stream.read_line(stream) 103 | let assert Ok(Nil) = file_stream.close(stream) 104 | } 105 | ``` 106 | 107 | ### API Documentation 108 | 109 | API documentation can be found at . 110 | 111 | ## License 112 | 113 | This library is published under the MIT license, a copy of which is included. 114 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "file_streams" 2 | description = "Gleam library for working with file streams." 3 | gleam = ">= 1.11.0" 4 | licences = ["MIT"] 5 | repository = { type = "github", user = "richard-viney", repo = "file_streams" } 6 | links = [ 7 | { title = "Website", href = "https://github.com/richard-viney/file_streams" }, 8 | ] 9 | 10 | [dependencies] 11 | gleam_stdlib = ">= 0.60.0 and < 2.0.0" 12 | 13 | [dev-dependencies] 14 | gleeunit = ">= 1.3.1 and < 2.0.0" 15 | simplifile = ">= 2.2.0 and < 3.0.0" 16 | 17 | [javascript] 18 | deno = { allow_read = true, allow_write = true } 19 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 6 | { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, 7 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" }, 8 | { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, 9 | ] 10 | 11 | [requirements] 12 | gleam_stdlib = { version = ">= 0.60.0 and < 2.0.0" } 13 | gleeunit = { version = ">= 1.3.1 and < 2.0.0" } 14 | simplifile = { version = ">= 2.2.0 and < 3.0.0" } 15 | -------------------------------------------------------------------------------- /src/file_streams/file_open_mode.gleam: -------------------------------------------------------------------------------- 1 | import file_streams/text_encoding.{type TextEncoding} 2 | 3 | /// The modes that can be specified when opening a file stream with 4 | /// [`file_stream.open()`](./file_stream.html#open). 5 | /// 6 | pub type FileOpenMode { 7 | /// The file is opened for writing. It is created if it does not exist. Every 8 | /// write operation to a file opened with `Append` takes place at the end of 9 | /// the file. 10 | Append 11 | 12 | /// Causes read operations on the file stream to return binaries rather than 13 | /// lists. 14 | /// 15 | /// This mode is always set by [`file_stream.open()`](./file_stream.html#open) 16 | /// and does not need to be specified manually. 17 | Binary 18 | 19 | /// Data in subsequent `file_stream.write_*` calls are buffered until at least 20 | /// `size` bytes are buffered, or until the oldest buffered data is `delay` 21 | /// milliseconds old. Then all buffered data is written in one operating 22 | /// system call. The buffered data is also flushed before some other file 23 | /// operations that are not `file_stream.write_*` calls. 24 | /// 25 | /// The purpose of this option is to increase performance by reducing the 26 | /// number of operating system calls. Thus, `file_stream.write_*` calls must 27 | /// be for sizes significantly less than `size`, and should not interspersed 28 | /// by too many other file operations. 29 | /// 30 | /// When this option is used, the result of `file_stream.write_*` calls can 31 | /// prematurely be reported as successful, and if a write error occurs, the 32 | /// error is reported as the result of the next file operation, which is not 33 | /// executed. 34 | /// 35 | /// For example, when `DelayedWrite` is used, after a number of 36 | /// `file_stream.write_*` calls, 37 | /// [`file_stream.close()`](./file_stream.html#close) can return 38 | /// `Error(FileStreamError(Enospc)))` as there is not enough space on the 39 | /// device for previously written data. 40 | /// [`file_stream.close()`](./file_stream.html#close) must be called again, as 41 | /// the file is still open. 42 | /// 43 | /// This mode is ignored on the JavaScript target. 44 | DelayedWrite(size: Int, delay: Int) 45 | 46 | /// Makes the file stream perform automatic translation of text to and from 47 | /// the specified text encoding when using the 48 | /// [`file_stream.read_line()`](./file_stream.html#read_line), 49 | /// [`file_stream.read_chars()`](./file_stream.html#read_chars), and 50 | /// [`file_stream.write_chars()`](./file_stream.html#write_chars) functions. 51 | /// 52 | /// If characters are written that can't be converted to the specified 53 | /// encoding then an error occurs and the file is closed. 54 | /// 55 | /// This option is not allowed when `Raw` is specified. 56 | /// 57 | /// The text encoding of an open file stream can be changed with 58 | /// [`file_stream.set_encoding()`](./file_stream.html#set_encoding) function. 59 | /// 60 | /// This mode is not supported on the JavaScript target. 61 | Encoding(encoding: TextEncoding) 62 | 63 | /// The file is opened for writing. It is created if it does not exist. If the 64 | /// file exists, `Error(FileStreamError(Eexist))` is returned by 65 | /// [`file_stream.open()`](./file_stream.html#open). 66 | /// 67 | /// This option does not guarantee exclusiveness on file systems not 68 | /// supporting `O_EXCL` properly, such as NFS. Do not depend on this option 69 | /// unless you know that the file system supports it (in general, local file 70 | /// systems are safe). 71 | /// 72 | /// This mode is ignored on the JavaScript target. 73 | Exclusive 74 | 75 | /// Allows much faster access to a file, as no Erlang process is needed to 76 | /// handle the file. However, a file opened in this way has the following 77 | /// limitations: 78 | /// 79 | /// - Only the Erlang process that opened the file can use it. 80 | /// - The `Encoding` option can't be used and text-based reading and writing 81 | /// is always done in UTF-8. This is because other text encodings depend on 82 | /// the `io` module, which requires an Erlang process to talk to. 83 | /// - The [`file_stream.read_chars()`](./file_stream.html#read_chars) function 84 | /// can't be used and will return `Error(Enotsup)`. 85 | /// - A remote Erlang file server cannot be used. The computer on which the 86 | /// Erlang node is running must have access to the file system (directly or 87 | /// through NFS). 88 | /// 89 | /// This mode is ignored on the JavaScript target. 90 | Raw 91 | 92 | /// The file, which must exist, is opened for reading. 93 | Read 94 | 95 | /// Activates read data buffering. If `file_stream.read_*` calls are for 96 | /// significantly less than `size` bytes, read operations to the operating 97 | /// system are still performed for blocks of `size` bytes. The extra data is 98 | /// buffered and returned in subsequent `file_stream.read_*` calls, giving a 99 | /// performance gain as the number of operating system calls is reduced. 100 | /// 101 | /// If `file_stream.read_*` calls are for sizes not significantly less than 102 | /// `size` bytes, or are greater than `size` bytes, no performance gain can be 103 | /// expected. 104 | /// 105 | /// This mode is ignored on the JavaScript target. 106 | ReadAhead(size: Int) 107 | 108 | /// The file is opened for writing. It is created if it does not exist. If the 109 | /// file exists and `Write` is not combined with `Read`, the file is 110 | /// truncated. 111 | Write 112 | } 113 | -------------------------------------------------------------------------------- /src/file_streams/file_stream.gleam: -------------------------------------------------------------------------------- 1 | //// Work with file streams in Gleam. 2 | 3 | import file_streams/file_open_mode.{type FileOpenMode} 4 | import file_streams/file_stream_error.{type FileStreamError} 5 | import file_streams/internal/raw_location 6 | import file_streams/internal/raw_read_result.{type RawReadResult} 7 | import file_streams/internal/raw_result.{type RawResult} 8 | import file_streams/text_encoding.{type TextEncoding, Latin1} 9 | import gleam/bit_array 10 | import gleam/bool 11 | import gleam/list 12 | import gleam/option.{type Option, None, Some} 13 | import gleam/result 14 | 15 | type IoDevice 16 | 17 | /// A file stream that data can be read from and/or written to depending on the 18 | /// modes specified when it was opened. 19 | /// 20 | pub opaque type FileStream { 21 | FileStream(io_device: IoDevice, encoding: Option(TextEncoding)) 22 | } 23 | 24 | /// Opens a new file stream that can read and/or write data from the specified 25 | /// file. See [`FileOpenMode`](./file_open_mode.html#FileOpenMode) for all of 26 | /// the available file modes. 27 | /// 28 | /// For simple cases of opening a file stream prefer one of the 29 | /// [`open_read()`](#open_read), [`open_write()`](#open_write), 30 | /// [`open_read_text()`](#open_read_text), or 31 | /// [`open_write_text()`](#open_write_text) helper functions to avoid needing to 32 | /// manually specify the file mode. 33 | /// 34 | /// Once the file stream is no longer needed it should be closed with 35 | /// [`close()`](#close). 36 | /// 37 | pub fn open( 38 | filename: String, 39 | modes: List(FileOpenMode), 40 | ) -> Result(FileStream, FileStreamError) { 41 | let is_raw = modes |> list.contains(file_open_mode.Raw) 42 | 43 | // Find the text encoding, if one was specified 44 | let encoding = 45 | modes 46 | |> list.find_map(fn(m) { 47 | case m { 48 | file_open_mode.Encoding(e) -> Ok(e) 49 | _ -> Error(Nil) 50 | } 51 | }) 52 | |> option.from_result 53 | 54 | let encoding = case is_raw, encoding { 55 | // Raw mode is not allowed when specifying a text encoding, as per the 56 | // Erlang docs, so turn it into an explicit error 57 | True, Some(_) -> Error(file_stream_error.Enotsup) 58 | 59 | True, None -> Ok(None) 60 | False, _ -> Ok(encoding |> option.or(Some(text_encoding.Latin1))) 61 | } 62 | use encoding <- result.try(encoding) 63 | 64 | // Binary mode is forced on so the Erlang APIs return binaries rather than 65 | // lists 66 | let mode = case modes |> list.contains(file_open_mode.Binary) { 67 | True -> modes 68 | False -> [file_open_mode.Binary, ..modes] 69 | } 70 | 71 | use io_device <- result.try(do_open(filename, mode)) 72 | 73 | Ok(FileStream(io_device, encoding)) 74 | } 75 | 76 | @external(erlang, "file", "open") 77 | @external(javascript, "../file_streams_ffi.mjs", "file_open") 78 | fn do_open( 79 | filename: String, 80 | mode: List(FileOpenMode), 81 | ) -> Result(IoDevice, FileStreamError) 82 | 83 | /// Opens a new file stream for reading from the specified file. Allows for 84 | /// efficient reading of binary data and lines of UTF-8 text. 85 | /// 86 | /// The modes used are: 87 | /// 88 | /// - `Read` 89 | /// - `ReadAhead(size: 64 * 1024)` 90 | /// - `Raw` 91 | /// 92 | pub fn open_read(filename: String) -> Result(FileStream, FileStreamError) { 93 | let modes = [ 94 | file_open_mode.Read, 95 | file_open_mode.ReadAhead(64 * 1024), 96 | file_open_mode.Raw, 97 | ] 98 | 99 | open(filename, modes) 100 | } 101 | 102 | /// Opens a new file stream for reading encoded text from a file. If only 103 | /// reading of UTF-8 lines of text is needed then prefer 104 | /// [`open_read()`](#open_read) as it is much faster due to using `Raw` mode. 105 | /// 106 | /// The modes used are: 107 | /// 108 | /// - `Read` 109 | /// - `ReadAhead(size: 64 * 1024)` 110 | /// - `Encoding(encoding)` 111 | /// 112 | /// The text encoding for a file stream can be changed with 113 | /// [`set_encoding`](#set_encoding). 114 | /// 115 | /// This function is not supported on the JavaScript target. 116 | /// 117 | pub fn open_read_text( 118 | filename: String, 119 | encoding: TextEncoding, 120 | ) -> Result(FileStream, FileStreamError) { 121 | let modes = [ 122 | file_open_mode.Read, 123 | file_open_mode.ReadAhead(64 * 1024), 124 | file_open_mode.Encoding(encoding), 125 | ] 126 | 127 | open(filename, modes) 128 | } 129 | 130 | /// Opens a new file stream for writing to a file. Allows for efficient writing 131 | /// of binary data and UTF-8 text. 132 | /// 133 | /// The modes used are: 134 | /// 135 | /// - `Write` 136 | /// - `DelayedWrite(size: 64 * 1024, delay: 2000)` 137 | /// - `Raw` 138 | /// 139 | pub fn open_write(filename: String) -> Result(FileStream, FileStreamError) { 140 | let modes = [ 141 | file_open_mode.Write, 142 | file_open_mode.DelayedWrite(size: 64 * 1024, delay: 2000), 143 | file_open_mode.Raw, 144 | ] 145 | 146 | open(filename, modes) 147 | } 148 | 149 | /// Opens a new file stream for writing encoded text to a file. If only writing 150 | /// of UTF-8 text is needed then prefer [`open_write()`](#open_write) as it is 151 | /// much faster due to using `Raw` mode. 152 | /// 153 | /// The modes used are: 154 | /// 155 | /// - `Write` 156 | /// - `DelayedWrite(size: 64 * 1024, delay: 2000)` 157 | /// - `Encoding(encoding)` 158 | /// 159 | /// The text encoding for a file stream can be changed with 160 | /// [`set_encoding`](#set_encoding). 161 | /// 162 | /// This function is not supported on the JavaScript target. 163 | /// 164 | pub fn open_write_text( 165 | filename: String, 166 | encoding: TextEncoding, 167 | ) -> Result(FileStream, FileStreamError) { 168 | let modes = [ 169 | file_open_mode.Write, 170 | file_open_mode.DelayedWrite(size: 64 * 1024, delay: 2000), 171 | file_open_mode.Encoding(encoding), 172 | ] 173 | 174 | open(filename, modes) 175 | } 176 | 177 | /// Closes an open file stream. 178 | /// 179 | pub fn close(stream: FileStream) -> Result(Nil, FileStreamError) { 180 | case file_close(stream.io_device) { 181 | raw_result.Ok -> Ok(Nil) 182 | raw_result.Error(e) -> Error(e) 183 | } 184 | } 185 | 186 | @external(erlang, "file", "close") 187 | @external(javascript, "../file_streams_ffi.mjs", "file_close") 188 | fn file_close(io_device: IoDevice) -> RawResult 189 | 190 | /// Changes the text encoding of a file stream from what was configured when it 191 | /// was opened. Returns a new [`FileStream`](#FileStream) that should be used 192 | /// for subsequent calls. 193 | /// 194 | /// This function is not supported for file streams opened in `Raw` mode. 195 | /// 196 | /// This function is not supported on the JavaScript target. 197 | /// 198 | pub fn set_encoding( 199 | stream: FileStream, 200 | encoding: TextEncoding, 201 | ) -> Result(FileStream, FileStreamError) { 202 | use <- bool.guard(stream.encoding == None, Error(file_stream_error.Enotsup)) 203 | 204 | let opts = [file_open_mode.Binary, file_open_mode.Encoding(encoding)] 205 | 206 | case io_setopts(stream.io_device, opts) { 207 | raw_result.Ok -> Ok(FileStream(..stream, encoding: Some(encoding))) 208 | raw_result.Error(e) -> Error(e) 209 | } 210 | } 211 | 212 | @external(erlang, "io", "setopts") 213 | @external(javascript, "../file_streams_ffi.mjs", "io_setopts") 214 | fn io_setopts(io_device: IoDevice, opts: List(FileOpenMode)) -> RawResult 215 | 216 | /// A file stream location defined relative to the beginning of the file, 217 | /// the end of the file, or the current position in the file stream. This type 218 | /// is used with the [`position()`](#position) function. 219 | /// 220 | pub type FileStreamLocation { 221 | /// A location relative to the beginning of the file, i.e. an absolute offset 222 | /// in the file stream. The offset should not be negative. 223 | BeginningOfFile(offset: Int) 224 | 225 | /// A location relative to the current position in the file stream. The offset 226 | /// can be either positive or negative. 227 | CurrentLocation(offset: Int) 228 | 229 | /// A location relative to the end of the file stream. The offset should not 230 | /// be positive. 231 | EndOfFile(offset: Int) 232 | } 233 | 234 | /// Sets the position of a file stream to the given location, where the location 235 | /// can be relative to the beginning of the file, the end of the file, or the 236 | /// current position in the file. On success, returns the current position in 237 | /// the file stream as an absolute offset in bytes. 238 | /// 239 | /// If a file stream is opened in `Append` mode then data is always written at 240 | /// the end of the file, regardless of the current file position. 241 | /// 242 | pub fn position( 243 | stream: FileStream, 244 | location: FileStreamLocation, 245 | ) -> Result(Int, FileStreamError) { 246 | let location = case location { 247 | BeginningOfFile(offset) -> raw_location.Bof(offset) 248 | CurrentLocation(offset) -> raw_location.Cur(offset) 249 | EndOfFile(offset) -> raw_location.Eof(offset) 250 | } 251 | 252 | file_position(stream.io_device, location) 253 | } 254 | 255 | @external(erlang, "file", "position") 256 | @external(javascript, "../file_streams_ffi.mjs", "file_position") 257 | fn file_position( 258 | io_device: IoDevice, 259 | location: raw_location.Location, 260 | ) -> Result(Int, FileStreamError) 261 | 262 | /// Writes raw bytes to a file stream. 263 | /// 264 | /// This function is supported when the file stream was opened in `Raw` mode or 265 | /// it uses the default `Latin1` text encoding. If this is not the case then 266 | /// use [`write_chars()`](#write_chars). 267 | /// 268 | pub fn write_bytes( 269 | stream: FileStream, 270 | bytes: BitArray, 271 | ) -> Result(Nil, FileStreamError) { 272 | use <- bool.guard( 273 | stream.encoding != None && stream.encoding != Some(Latin1), 274 | Error(file_stream_error.Enotsup), 275 | ) 276 | 277 | // Check that the bit array contains a whole number of bytes 278 | use <- bool.guard( 279 | bit_array.bit_size(bytes) % 8 != 0, 280 | Error(file_stream_error.Einval), 281 | ) 282 | 283 | case file_write(stream.io_device, bytes) { 284 | raw_result.Ok -> Ok(Nil) 285 | raw_result.Error(e) -> Error(e) 286 | } 287 | } 288 | 289 | @external(erlang, "file", "write") 290 | @external(javascript, "../file_streams_ffi.mjs", "file_write") 291 | fn file_write(io_device: IoDevice, bytes: BitArray) -> RawResult 292 | 293 | /// Writes characters to a file stream. This will convert the characters to the 294 | /// text encoding specified when the file stream was opened. 295 | /// 296 | /// For file streams opened in `Raw` mode, this function always writes UTF-8. 297 | /// 298 | /// This function is not supported on the JavaScript target. 299 | /// 300 | pub fn write_chars( 301 | stream: FileStream, 302 | chars: String, 303 | ) -> Result(Nil, FileStreamError) { 304 | case stream.encoding { 305 | None -> chars |> bit_array.from_string |> write_bytes(stream, _) 306 | Some(_) -> io_put_chars(stream.io_device, chars) 307 | } 308 | } 309 | 310 | @external(erlang, "file_streams_ffi", "io_put_chars") 311 | @external(javascript, "../file_streams_ffi.mjs", "io_put_chars") 312 | fn io_put_chars( 313 | io_device: IoDevice, 314 | char_data: String, 315 | ) -> Result(Nil, FileStreamError) 316 | 317 | /// Syncs a file stream that was opened for writing. This ensures that any write 318 | /// buffers kept by the operating system (not by the Erlang runtime system) are 319 | /// written to disk. 320 | /// 321 | /// When a file stream is opened with delayed writes enabled to improve 322 | /// performance, syncing can return an error related to flushing recently 323 | /// written data to the underlying device. 324 | /// 325 | pub fn sync(stream: FileStream) -> Result(Nil, FileStreamError) { 326 | case file_sync(stream.io_device) { 327 | raw_result.Ok -> Ok(Nil) 328 | raw_result.Error(e) -> Error(e) 329 | } 330 | } 331 | 332 | @external(erlang, "file", "sync") 333 | @external(javascript, "../file_streams_ffi.mjs", "file_sync") 334 | fn file_sync(io_device: IoDevice) -> RawResult 335 | 336 | /// Reads bytes from a file stream. The returned number of bytes may be fewer 337 | /// than the number that was requested if the end of the file stream was 338 | /// reached. 339 | /// 340 | /// If the end of the file stream is encountered before any bytes can be read 341 | /// then `Error(Eof)` is returned. 342 | /// 343 | /// This function is supported when the file stream was opened in `Raw` mode or 344 | /// it uses the default `Latin1` text encoding. If this is not the case then 345 | /// use [`read_chars()`](#read_chars) or [`read_line()`](#read_line). 346 | /// 347 | pub fn read_bytes( 348 | stream: FileStream, 349 | byte_count: Int, 350 | ) -> Result(BitArray, FileStreamError) { 351 | use <- bool.guard( 352 | stream.encoding != None && stream.encoding != Some(Latin1), 353 | Error(file_stream_error.Enotsup), 354 | ) 355 | 356 | case file_read(stream.io_device, byte_count) { 357 | raw_read_result.Ok(bytes) -> Ok(bytes) 358 | raw_read_result.Eof -> Error(file_stream_error.Eof) 359 | raw_read_result.Error(e) -> Error(e) 360 | } 361 | } 362 | 363 | @external(erlang, "file", "read") 364 | @external(javascript, "../file_streams_ffi.mjs", "file_read") 365 | fn file_read(io_device: IoDevice, byte_count: Int) -> RawReadResult(BitArray) 366 | 367 | /// Reads the requested number of bytes from a file stream. If the requested 368 | /// number of bytes can't be read prior to reaching the end of the file stream 369 | /// then `Error(Eof)` is returned. 370 | /// 371 | /// This function is supported when the file stream was opened in `Raw` mode or 372 | /// it uses the default `Latin1` text encoding. If this is not the case then use 373 | /// [`read_chars()`](#read_chars) or [`read_line()`](#read_line) should be used 374 | /// instead. 375 | /// 376 | pub fn read_bytes_exact( 377 | stream: FileStream, 378 | byte_count: Int, 379 | ) -> Result(BitArray, FileStreamError) { 380 | case read_bytes(stream, byte_count) { 381 | Ok(bytes) -> 382 | case bit_array.byte_size(bytes) == byte_count { 383 | True -> Ok(bytes) 384 | False -> Error(file_stream_error.Eof) 385 | } 386 | 387 | error -> error 388 | } 389 | } 390 | 391 | /// Reads all remaining bytes from a file stream. If no more data is available 392 | /// in the file stream then this function will return an empty bit array. It 393 | /// never returns `Error(Eof)`. 394 | /// 395 | /// This function is supported when the file stream was opened in `Raw` mode or 396 | /// it uses the default `Latin1` text encoding. If this is not the case then use 397 | /// [`read_chars()`](#read_chars) or [`read_line()`](#read_line) should be used 398 | /// instead. 399 | /// 400 | pub fn read_remaining_bytes( 401 | stream: FileStream, 402 | ) -> Result(BitArray, FileStreamError) { 403 | do_read_remaining_bytes(stream, []) 404 | } 405 | 406 | fn do_read_remaining_bytes( 407 | stream: FileStream, 408 | acc: List(BitArray), 409 | ) -> Result(BitArray, FileStreamError) { 410 | case read_bytes(stream, 64 * 1024) { 411 | Ok(bytes) -> do_read_remaining_bytes(stream, [bytes, ..acc]) 412 | 413 | Error(file_stream_error.Eof) -> 414 | acc 415 | |> list.reverse 416 | |> bit_array.concat 417 | |> Ok 418 | 419 | Error(e) -> Error(e) 420 | } 421 | } 422 | 423 | /// Reads the next line of text from a file stream. The returned string 424 | /// will include the newline `\n` character. If the stream contains a Windows 425 | /// newline `\r\n` then only the `\n` will be returned. 426 | /// 427 | /// This function always reads UTF-8 for file streams opened in `Raw` mode. 428 | /// Otherwise, it uses the text encoding specified when the file was opened. 429 | /// 430 | /// This function is not supported on the JavaScript target. 431 | /// 432 | pub fn read_line(stream: FileStream) -> Result(String, FileStreamError) { 433 | case stream.encoding { 434 | None -> 435 | case file_read_line(stream.io_device) { 436 | raw_read_result.Ok(data) -> 437 | data 438 | |> bit_array.to_string 439 | |> result.replace_error(file_stream_error.InvalidUnicode) 440 | 441 | raw_read_result.Eof -> Error(file_stream_error.Eof) 442 | raw_read_result.Error(e) -> Error(e) 443 | } 444 | 445 | Some(_) -> 446 | case io_get_line(stream.io_device) { 447 | raw_read_result.Ok(data) -> Ok(data) 448 | raw_read_result.Eof -> Error(file_stream_error.Eof) 449 | raw_read_result.Error(e) -> Error(e) 450 | } 451 | } 452 | } 453 | 454 | @external(erlang, "file_streams_ffi", "io_get_line") 455 | @external(javascript, "../file_streams_ffi.mjs", "io_get_line") 456 | fn io_get_line(io_device: IoDevice) -> RawReadResult(String) 457 | 458 | @external(erlang, "file", "read_line") 459 | @external(javascript, "../file_streams_ffi.mjs", "file_read_line") 460 | fn file_read_line(io_device: IoDevice) -> RawReadResult(BitArray) 461 | 462 | /// Reads the next `count` characters from a file stream. The returned number of 463 | /// characters may be fewer than the number that was requested if the end of the 464 | /// stream is reached. 465 | /// 466 | /// This function is not supported for file streams opened in `Raw` mode. Use 467 | /// the [`read_line()`](#read_line) function instead. 468 | /// 469 | /// This function is not supported on the JavaScript target. 470 | /// 471 | pub fn read_chars( 472 | stream: FileStream, 473 | count: Int, 474 | ) -> Result(String, FileStreamError) { 475 | case stream.encoding { 476 | Some(_) -> 477 | case io_get_chars(stream.io_device, count) { 478 | raw_read_result.Ok(data) -> Ok(data) 479 | raw_read_result.Eof -> Error(file_stream_error.Eof) 480 | raw_read_result.Error(e) -> Error(e) 481 | } 482 | 483 | None -> Error(file_stream_error.Enotsup) 484 | } 485 | } 486 | 487 | @external(erlang, "file_streams_ffi", "io_get_chars") 488 | @external(javascript, "../file_streams_ffi.mjs", "io_get_chars") 489 | fn io_get_chars(io_device: IoDevice, count: Int) -> RawReadResult(String) 490 | 491 | /// Reads an 8-bit signed integer from a file stream. 492 | /// 493 | pub fn read_int8(stream: FileStream) -> Result(Int, FileStreamError) { 494 | use bits <- result.map(read_bytes_exact(stream, 1)) 495 | 496 | let assert <> = bits 497 | v 498 | } 499 | 500 | /// Reads an 8-bit unsigned integer from a file stream. 501 | /// 502 | pub fn read_uint8(stream: FileStream) -> Result(Int, FileStreamError) { 503 | use bits <- result.map(read_bytes_exact(stream, 1)) 504 | 505 | let assert <> = bits 506 | v 507 | } 508 | 509 | /// Reads a little-endian 16-bit signed integer from a file stream. 510 | /// 511 | pub fn read_int16_le(stream: FileStream) -> Result(Int, FileStreamError) { 512 | use bits <- result.map(read_bytes_exact(stream, 2)) 513 | 514 | let assert <> = bits 515 | v 516 | } 517 | 518 | /// Reads a big-endian 16-bit signed integer from a file stream. 519 | /// 520 | pub fn read_int16_be(stream: FileStream) -> Result(Int, FileStreamError) { 521 | use bits <- result.map(read_bytes_exact(stream, 2)) 522 | 523 | let assert <> = bits 524 | v 525 | } 526 | 527 | /// Reads a little-endian 16-bit unsigned integer from a file stream. 528 | /// 529 | pub fn read_uint16_le(stream: FileStream) -> Result(Int, FileStreamError) { 530 | use bits <- result.map(read_bytes_exact(stream, 2)) 531 | 532 | let assert <> = bits 533 | v 534 | } 535 | 536 | /// Reads a big-endian 16-bit unsigned integer from a file stream. 537 | /// 538 | pub fn read_uint16_be(stream: FileStream) -> Result(Int, FileStreamError) { 539 | use bits <- result.map(read_bytes_exact(stream, 2)) 540 | 541 | let assert <> = bits 542 | v 543 | } 544 | 545 | /// Reads a little-endian 32-bit signed integer from a file stream. 546 | /// 547 | pub fn read_int32_le(stream: FileStream) -> Result(Int, FileStreamError) { 548 | use bits <- result.map(read_bytes_exact(stream, 4)) 549 | 550 | let assert <> = bits 551 | v 552 | } 553 | 554 | /// Reads a big-endian 32-bit signed integer from a file stream. 555 | /// 556 | pub fn read_int32_be(stream: FileStream) -> Result(Int, FileStreamError) { 557 | use bits <- result.map(read_bytes_exact(stream, 4)) 558 | 559 | let assert <> = bits 560 | v 561 | } 562 | 563 | /// Reads a little-endian 32-bit unsigned integer from a file stream. 564 | /// 565 | pub fn read_uint32_le(stream: FileStream) -> Result(Int, FileStreamError) { 566 | use bits <- result.map(read_bytes_exact(stream, 4)) 567 | 568 | let assert <> = bits 569 | v 570 | } 571 | 572 | /// Reads a big-endian 32-bit unsigned integer from a file stream. 573 | /// 574 | pub fn read_uint32_be(stream: FileStream) -> Result(Int, FileStreamError) { 575 | use bits <- result.map(read_bytes_exact(stream, 4)) 576 | 577 | let assert <> = bits 578 | v 579 | } 580 | 581 | /// Reads a little-endian 64-bit signed integer from a file stream. 582 | /// 583 | pub fn read_int64_le(stream: FileStream) -> Result(Int, FileStreamError) { 584 | use bits <- result.map(read_bytes_exact(stream, 8)) 585 | 586 | let assert <> = bits 587 | v 588 | } 589 | 590 | /// Reads a big-endian 64-bit signed integer from a file stream. 591 | /// 592 | pub fn read_int64_be(stream: FileStream) -> Result(Int, FileStreamError) { 593 | use bits <- result.map(read_bytes_exact(stream, 8)) 594 | 595 | let assert <> = bits 596 | v 597 | } 598 | 599 | /// Reads a little-endian 64-bit unsigned integer from a file stream. 600 | /// 601 | pub fn read_uint64_le(stream: FileStream) -> Result(Int, FileStreamError) { 602 | use bits <- result.map(read_bytes_exact(stream, 8)) 603 | 604 | let assert <> = bits 605 | v 606 | } 607 | 608 | /// Reads a big-endian 64-bit unsigned integer from a file stream. 609 | /// 610 | pub fn read_uint64_be(stream: FileStream) -> Result(Int, FileStreamError) { 611 | use bits <- result.map(read_bytes_exact(stream, 8)) 612 | 613 | let assert <> = bits 614 | v 615 | } 616 | 617 | /// Reads a little-endian 32-bit float from a file stream. 618 | /// 619 | pub fn read_float32_le(stream: FileStream) -> Result(Float, FileStreamError) { 620 | use bits <- result.map(read_bytes_exact(stream, 4)) 621 | 622 | let assert <> = bits 623 | v 624 | } 625 | 626 | /// Reads a big-endian 32-bit float from a file stream. 627 | /// 628 | pub fn read_float32_be(stream: FileStream) -> Result(Float, FileStreamError) { 629 | use bits <- result.map(read_bytes_exact(stream, 4)) 630 | 631 | let assert <> = bits 632 | v 633 | } 634 | 635 | /// Reads a little-endian 64-bit float from a file stream. 636 | /// 637 | pub fn read_float64_le(stream: FileStream) -> Result(Float, FileStreamError) { 638 | use bits <- result.map(read_bytes_exact(stream, 8)) 639 | 640 | let assert <> = bits 641 | v 642 | } 643 | 644 | /// Reads a big-endian 64-bit float from a file stream. 645 | /// 646 | pub fn read_float64_be(stream: FileStream) -> Result(Float, FileStreamError) { 647 | use bits <- result.map(read_bytes_exact(stream, 8)) 648 | 649 | let assert <> = bits 650 | v 651 | } 652 | 653 | /// Reads the specified type the requested number of times from a file stream, 654 | /// e.g. two little-endian 32-bit integers, or four big-endian 64-bit floats, 655 | /// and returns the values in a list. 656 | /// 657 | /// ## Examples 658 | /// 659 | /// ```gleam 660 | /// read_list(stream, read_int32_le, 2) 661 | /// |> Ok([1, 2]) 662 | /// 663 | /// read_list(stream, read_float64_be, 4) 664 | /// |> Ok([1.0, 2.0]) 665 | /// ``` 666 | /// 667 | pub fn read_list( 668 | stream: FileStream, 669 | item_read_fn: fn(FileStream) -> Result(a, FileStreamError), 670 | item_count: Int, 671 | ) -> Result(List(a), FileStreamError) { 672 | do_read_list(stream, item_read_fn, item_count, []) 673 | |> result.map(list.reverse) 674 | } 675 | 676 | fn do_read_list( 677 | stream: FileStream, 678 | item_read_fn: fn(FileStream) -> Result(a, FileStreamError), 679 | item_count: Int, 680 | acc: List(a), 681 | ) -> Result(List(a), FileStreamError) { 682 | case item_count { 683 | 0 -> Ok(acc) 684 | _ -> 685 | case item_read_fn(stream) { 686 | Ok(item) -> 687 | do_read_list(stream, item_read_fn, item_count - 1, [item, ..acc]) 688 | Error(e) -> Error(e) 689 | } 690 | } 691 | } 692 | -------------------------------------------------------------------------------- /src/file_streams/file_stream_error.gleam: -------------------------------------------------------------------------------- 1 | import file_streams/text_encoding.{type TextEncoding} 2 | 3 | /// The reasons why a file stream operation can fail. Most of these map to 4 | /// underlying POSIX errors. 5 | /// 6 | pub type FileStreamError { 7 | /// Permission denied. 8 | Eacces 9 | 10 | /// Resource temporarily unavailable. 11 | Eagain 12 | 13 | /// Bad file number 14 | Ebadf 15 | 16 | /// Bad message. 17 | Ebadmsg 18 | 19 | /// File busy. 20 | Ebusy 21 | 22 | /// Resource deadlock avoided. 23 | Edeadlk 24 | 25 | /// On most architectures, same as `Edeadlk`. On some architectures, it 26 | /// 27 | /// means "File locking deadlock error." 28 | Edeadlock 29 | 30 | /// Disk quota exceeded. 31 | Edquot 32 | 33 | /// File already exists. 34 | Eexist 35 | 36 | /// Bad address in system call argument. 37 | Efault 38 | 39 | /// File too large. 40 | Efbig 41 | 42 | /// Inappropriate file type or format. Usually caused by trying to set the 43 | /// 44 | /// "sticky bit" on a regular file (not a directory). 45 | Eftype 46 | 47 | /// Interrupted system call. 48 | Eintr 49 | 50 | /// Invalid argument. 51 | Einval 52 | 53 | /// I/O error. 54 | Eio 55 | 56 | /// Illegal operation on a directory. 57 | Eisdir 58 | 59 | /// Too many levels of symbolic links. 60 | Eloop 61 | 62 | /// Too many open files. 63 | Emfile 64 | 65 | /// Too many links. 66 | Emlink 67 | 68 | /// Multihop attempted. 69 | Emultihop 70 | 71 | /// Filename too long 72 | Enametoolong 73 | 74 | /// File table overflow 75 | Enfile 76 | 77 | /// No buffer space available. 78 | Enobufs 79 | 80 | /// No such device. 81 | Enodev 82 | 83 | /// No locks available. 84 | Enolck 85 | 86 | /// Link has been severed. 87 | Enolink 88 | 89 | /// No such file or directory. 90 | Enoent 91 | 92 | /// Not enough memory. 93 | Enomem 94 | 95 | /// No space left on device. 96 | Enospc 97 | 98 | /// No STREAM resources. 99 | Enosr 100 | 101 | /// Not a STREAM. 102 | Enostr 103 | 104 | /// Function not implemented. 105 | Enosys 106 | 107 | /// Block device required. 108 | Enotblk 109 | 110 | /// Not a directory. 111 | Enotdir 112 | 113 | /// Operation not supported. 114 | Enotsup 115 | 116 | /// No such device or address. 117 | Enxio 118 | 119 | /// Operation not supported on socket. 120 | Eopnotsupp 121 | 122 | /// Value too large to be stored in data type. 123 | Eoverflow 124 | 125 | /// Not owner. 126 | Eperm 127 | 128 | /// Broken pipe. 129 | Epipe 130 | 131 | /// Result too large. 132 | Erange 133 | 134 | /// Read-only file system. 135 | Erofs 136 | 137 | /// Invalid seek. 138 | Espipe 139 | 140 | /// No such process. 141 | Esrch 142 | 143 | /// Stale remote file handle. 144 | Estale 145 | 146 | /// Text file busy. 147 | Etxtbsy 148 | 149 | /// Cross-domain link. 150 | Exdev 151 | 152 | /// The end of the file stream was reached before the requested data could 153 | /// be read. 154 | Eof 155 | 156 | /// Text data was encountered that can't be converted from/to the relevant 157 | /// text encoding. E.g. trying to write Chinese characters to a file stream 158 | /// opened with the `Latin1` encoding. 159 | NoTranslation(from: TextEncoding, to: TextEncoding) 160 | 161 | /// Data was encountered that is not valid Unicode. E.g. invalid bytes in a 162 | /// UTF-8 text file. 163 | InvalidUnicode 164 | } 165 | 166 | /// Returns a human-readable description of a file stream error. 167 | /// 168 | pub fn describe(error: FileStreamError) -> String { 169 | case error { 170 | Eacces -> "Permission denied" 171 | Eagain -> "Resource temporarily unavailable" 172 | Ebadf -> "Bad file number" 173 | Ebadmsg -> "Bad message" 174 | Ebusy -> "File busy" 175 | Edeadlk -> "Resource deadlock avoided" 176 | Edeadlock -> "File locking deadlock error" 177 | Edquot -> "Disk quota exceeded" 178 | Eexist -> "File already exists" 179 | Efault -> "Bad address in system call argument" 180 | Efbig -> "File too large" 181 | Eftype -> "Inappropriate file type or format" 182 | Eintr -> "Interrupted system call" 183 | Einval -> "Invalid argument" 184 | Eio -> "I/O error" 185 | Eisdir -> "Illegal operation on a directory" 186 | Eloop -> "Too many levels of symbolic links" 187 | Emfile -> "Too many open files" 188 | Emlink -> "Too many links" 189 | Emultihop -> "Multihop attempted" 190 | Enametoolong -> "Filename too long" 191 | Enfile -> "File table overflow" 192 | Enobufs -> "No buffer space available" 193 | Enodev -> "No such device" 194 | Enoent -> "No such file or directory" 195 | Enolck -> "No locks available" 196 | Enolink -> "Link has been severed" 197 | Enomem -> "Not enough memory" 198 | Enospc -> "No space left on device" 199 | Enosr -> "No stream resources" 200 | Enostr -> "Not a stream" 201 | Enosys -> "Function not implemented" 202 | Enotblk -> "Block device required" 203 | Enotdir -> "Not a directory" 204 | Enotsup -> "Operation not supported" 205 | Enxio -> "No such device or address" 206 | Eopnotsupp -> "Operation not supported on socket" 207 | Eoverflow -> "Value too large to be stored in data type" 208 | Eperm -> "Permission denied due to file ownership" 209 | Epipe -> "Broken pipe" 210 | Erange -> "Result too large" 211 | Erofs -> "Read-only file system" 212 | Espipe -> "Invalid seek" 213 | Esrch -> "No such process" 214 | Estale -> "Stale remote file handle" 215 | Etxtbsy -> "Text file busy" 216 | Exdev -> "Cross-domain link" 217 | Eof -> "End of file stream" 218 | NoTranslation(..) -> "Unable to convert encoding" 219 | InvalidUnicode -> "Invalid bytes for Unicode encoding" 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/file_streams/internal/raw_location.gleam: -------------------------------------------------------------------------------- 1 | pub type Location { 2 | Bof(offset: Int) 3 | Cur(offset: Int) 4 | Eof(offset: Int) 5 | } 6 | -------------------------------------------------------------------------------- /src/file_streams/internal/raw_read_result.gleam: -------------------------------------------------------------------------------- 1 | import file_streams/file_stream_error.{type FileStreamError} 2 | 3 | pub type RawReadResult(a) { 4 | Ok(a) 5 | Eof 6 | Error(error: FileStreamError) 7 | } 8 | -------------------------------------------------------------------------------- /src/file_streams/internal/raw_result.gleam: -------------------------------------------------------------------------------- 1 | import file_streams/file_stream_error.{type FileStreamError} 2 | 3 | pub type RawResult { 4 | Ok 5 | Error(error: FileStreamError) 6 | } 7 | -------------------------------------------------------------------------------- /src/file_streams/text_encoding.gleam: -------------------------------------------------------------------------------- 1 | //// The encodings available for reading and writing text with a file stream. 2 | 3 | /// Text encoding for a file stream. The default encoding is `Latin1`. 4 | /// 5 | /// Text read from a file stream using 6 | /// [`file_stream.read_chars()`](./file_stream.html#read_chars) and 7 | /// [`file_stream.read_line()`](./file_stream.html#read_line) will be 8 | /// automatically converted from the specified encoding to a `String`. 9 | /// Similarly, text written to a file stream using 10 | /// [`file_stream.write_chars()`](./file_stream.html#write_chars) will be 11 | /// converted to the specified encoding before being written to a file stream. 12 | /// 13 | pub type TextEncoding { 14 | /// The Unicode UTF-8 text encoding. 15 | Unicode 16 | 17 | /// The ISO 8859-1 (Latin-1) text encoding. This is the default encoding. 18 | /// 19 | /// When using this encoding, 20 | /// [`file_stream.write_chars()`](./file_stream.html#write_chars) can only 21 | /// write Unicode codepoints up to `U+00FF`. 22 | Latin1 23 | 24 | /// The Unicode UTF-16 text encoding, with the specified byte ordering. 25 | Utf16(endianness: Endianness) 26 | 27 | /// The Unicode UTF-32 text encoding, with the specified byte ordering. 28 | Utf32(endianness: Endianness) 29 | } 30 | 31 | /// Endianness specifier used by the `Utf16` and `Utf32` text encodings. 32 | /// 33 | pub type Endianness { 34 | /// Big endian. 35 | Big 36 | 37 | /// Little endian. The most common endianness mode, use this if uncertain. 38 | Little 39 | } 40 | -------------------------------------------------------------------------------- /src/file_streams_ffi.erl: -------------------------------------------------------------------------------- 1 | -module(file_streams_ffi). 2 | -export([io_get_line/1, io_get_chars/2, io_put_chars/2]). 3 | 4 | % Wraps io:get_line to return `{ok, Data}` on success instead of just `Data` 5 | % 6 | io_get_line(Device) -> 7 | case io:get_line(Device, "") of 8 | eof -> eof; 9 | {error, Reason} -> {error, Reason}; 10 | Data -> {ok, Data} 11 | end. 12 | 13 | % Wraps io:get_chars to return `{ok, Data}` on success instead of just `Data` 14 | % 15 | io_get_chars(Device, Count) -> 16 | case io:get_chars(Device, "", Count) of 17 | eof -> eof; 18 | {error, Reason} -> {error, Reason}; 19 | Data -> {ok, Data} 20 | end. 21 | 22 | % Wraps io:put_chars to return `{ok, nil}` on success instead of just `ok`, and 23 | % to return the no_translation exception as an error. 24 | % 25 | io_put_chars(Device, CharData) -> 26 | try 27 | case io:put_chars(Device, CharData) of 28 | ok -> {ok, nil}; 29 | {error, Reason} -> {error, Reason} 30 | end 31 | catch 32 | error:no_translation -> {error, {no_translation, unicode, latin1}} 33 | end. 34 | -------------------------------------------------------------------------------- /src/file_streams_ffi.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | closeSync, 3 | existsSync, 4 | fsyncSync, 5 | openSync, 6 | readSync, 7 | statSync, 8 | writeSync, 9 | } from "node:fs"; 10 | import { BitArray, Ok, Error } from "./gleam.mjs"; 11 | import * as file_open_mode from "./file_streams/file_open_mode.mjs"; 12 | import * as raw_location from "./file_streams/internal/raw_location.mjs"; 13 | import * as raw_result from "./file_streams/internal/raw_result.mjs"; 14 | import * as raw_read_result from "./file_streams/internal/raw_read_result.mjs"; 15 | import * as file_stream_error from "./file_streams/file_stream_error.mjs"; 16 | 17 | export function file_open(filename, mode) { 18 | try { 19 | let size = 0; 20 | 21 | try { 22 | const stats = statSync(filename); 23 | 24 | // Return an error if the filename is a directory 25 | if (stats.isDirectory()) { 26 | return new Error(new file_stream_error.Eisdir()); 27 | } 28 | 29 | // Store size of the file if it exists so that seeks done relative to the 30 | // end of the file are possible 31 | size = stats.size; 32 | } catch {} 33 | 34 | // Read relevant settings from the mode 35 | mode = mode.toArray(); 36 | let mode_read = mode.some((mode) => mode instanceof file_open_mode.Read); 37 | let mode_write = mode.some((mode) => mode instanceof file_open_mode.Write); 38 | let mode_append = mode.some( 39 | (mode) => mode instanceof file_open_mode.Append 40 | ); 41 | 42 | // Append implies write 43 | mode_write ||= mode_append; 44 | 45 | // Default to read 46 | if (!mode_read && !mode_write) { 47 | mode_read = true; 48 | } 49 | 50 | // Text encodings are not supported on JavaScript 51 | if (mode.some((mode) => mode instanceof file_open_mode.Encoding)) { 52 | return new Error(new file_stream_error.Enotsup()); 53 | } 54 | 55 | // Determine the mode string 56 | let mode_string; 57 | if (mode_write) { 58 | if (mode_read) { 59 | if (existsSync(filename)) { 60 | mode_string = "r+"; 61 | } else { 62 | mode_string = "w+"; 63 | } 64 | } else { 65 | mode_string = "w"; 66 | } 67 | } else { 68 | mode_string = "r"; 69 | } 70 | 71 | // Open the file 72 | const fd = openSync(filename, mode_string); 73 | 74 | const io_device = { 75 | fd, 76 | position: 0, 77 | size, 78 | mode_append, 79 | }; 80 | 81 | return new Ok(io_device); 82 | } catch (e) { 83 | return new Error(map_error(e)); 84 | } 85 | } 86 | 87 | export function file_read(io_device, byte_count) { 88 | try { 89 | // Reading zero bytes always succeeds 90 | if (byte_count === 0) { 91 | return new raw_read_result.Ok(new BitArray(new Uint8Array())); 92 | } 93 | 94 | // Read bytes at the current position 95 | let buffer = new Uint8Array(byte_count); 96 | const bytes_read = readSync( 97 | io_device.fd, 98 | buffer, 99 | 0, 100 | byte_count, 101 | io_device.position 102 | ); 103 | 104 | // Advance the current position 105 | io_device.position += bytes_read; 106 | 107 | // Return eof if nothing was read 108 | if (bytes_read === 0) { 109 | return new raw_read_result.Eof(); 110 | } 111 | 112 | // Convert result to a BitArray 113 | let final_buffer = buffer; 114 | if (bytes_read < byte_count) { 115 | final_buffer = buffer.slice(0, bytes_read); 116 | } 117 | const bit_array = new BitArray(final_buffer); 118 | 119 | return new raw_read_result.Ok(bit_array); 120 | } catch (e) { 121 | return new raw_read_result.Error(map_error(e)); 122 | } 123 | } 124 | 125 | export function file_write(io_device, data) { 126 | if (data.bitSize % 8 !== 0) { 127 | return new raw_result.Error(file_stream_error.Einval()); 128 | } 129 | 130 | try { 131 | const position = io_device.mode_append 132 | ? io_device.size 133 | : io_device.position; 134 | 135 | let buffer = data.rawBuffer; 136 | if (data.bitOffset !== 0) { 137 | buffer = new Uint8Array(data.byteSize); 138 | for (let i = 0; i < data.byteSize; i++) { 139 | buffer[i] = data.byteAt(i); 140 | } 141 | } 142 | 143 | // Write data to the file 144 | const bytes_written = writeSync( 145 | io_device.fd, 146 | buffer, 147 | 0, 148 | buffer.length, 149 | position 150 | ); 151 | 152 | // Update the file's size and position depending if it is in append mode 153 | if (io_device.mode_append) { 154 | io_device.size += bytes_written; 155 | } else { 156 | io_device.position += bytes_written; 157 | if (io_device.position > io_device.size) { 158 | io_device.size = io_device.position; 159 | } 160 | } 161 | 162 | // Check for an incomplete write 163 | if (bytes_written !== data.byteSize) { 164 | return new raw_result.Error(new file_stream_error.Enospc()); 165 | } 166 | 167 | return new raw_result.Ok(); 168 | } catch (e) { 169 | return new raw_result.Error(map_error(e)); 170 | } 171 | } 172 | 173 | export function file_close(io_device) { 174 | try { 175 | closeSync(io_device.fd); 176 | 177 | io_device.fd = -1; 178 | 179 | return new raw_result.Ok(); 180 | } catch (e) { 181 | return new raw_result.Error(map_error(e)); 182 | } 183 | } 184 | 185 | export function file_position(io_device, location) { 186 | let new_position = location.offset; 187 | if (location instanceof raw_location.Eof) { 188 | new_position += io_device.size; 189 | } else if (location instanceof raw_location.Cur) { 190 | new_position += io_device.position; 191 | } 192 | 193 | if (new_position < 0) { 194 | return new Error(new file_stream_error.Einval()); 195 | } 196 | 197 | io_device.position = new_position; 198 | 199 | return new Ok(io_device.position); 200 | } 201 | 202 | export function file_sync(io_device) { 203 | try { 204 | fsyncSync(io_device.fd); 205 | 206 | return new Ok(undefined); 207 | } catch (e) { 208 | return new Error(map_error(e)); 209 | } 210 | } 211 | 212 | // 213 | // Functions that work with encoded text aren't supported on JavaScript. It 214 | // is likely possible to implement this in future if someone is interested. 215 | // 216 | 217 | export function file_read_line(_io_device) { 218 | return new raw_read_result.Error(new file_stream_error.Enotsup()); 219 | } 220 | 221 | export function io_get_line(_io_device) { 222 | return new raw_read_result.Error(new file_stream_error.Enotsup()); 223 | } 224 | 225 | export function io_get_chars(_io_device, _char_data) { 226 | return new raw_read_result.Error(new file_stream_error.Enotsup()); 227 | } 228 | 229 | export function io_put_chars(_io_device, _char_data) { 230 | return new Error(new file_stream_error.Enotsup()); 231 | } 232 | 233 | export function io_setopts(_io_device) { 234 | return new raw_result.Error(new file_stream_error.Enotsup()); 235 | } 236 | 237 | function map_error(error) { 238 | switch (error.code) { 239 | case "EACCES": 240 | return new file_stream_error.Eacces(); 241 | case "EBADF": 242 | return new file_stream_error.Ebadf(); 243 | case "EEXIST": 244 | return new file_stream_error.Eexist(); 245 | case "EISDIR": 246 | return new file_stream_error.Eisdir(); 247 | case "EMFILE": 248 | return new file_stream_error.Emfile(); 249 | case "ENOENT": 250 | return new file_stream_error.Enoent(); 251 | case "ENOTDIR": 252 | return new file_stream_error.Enotdir(); 253 | case "ENOSPC": 254 | return new file_stream_error.Enospc(); 255 | case "EPERM": 256 | return new file_stream_error.Eperm(); 257 | case "EROFS": 258 | return new file_stream_error.Erofs(); 259 | case "EIO": 260 | return new file_stream_error.Eio(); 261 | case "ENODEV": 262 | return new file_stream_error.Enodev(); 263 | case "ETXTBSY": 264 | return new file_stream_error.Etxtbsy(); 265 | case "EINVAL": 266 | return new file_stream_error.Einval(); 267 | case "EIO": 268 | return new file_stream_error.Eio(); 269 | case "ENFILE": 270 | return new file_stream_error.Enfile(); 271 | case undefined: 272 | throw `Undefined error code for error: ${error}`; 273 | default: 274 | throw `Unrecognized error code: ${error.code}`; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /test/file_stream_error_test.gleam: -------------------------------------------------------------------------------- 1 | import file_streams/file_stream_error 2 | 3 | pub fn describe_test() { 4 | assert file_stream_error.describe(file_stream_error.Eacces) 5 | == "Permission denied" 6 | } 7 | -------------------------------------------------------------------------------- /test/file_streams_test.gleam: -------------------------------------------------------------------------------- 1 | import file_streams/file_open_mode 2 | import file_streams/file_stream 3 | import file_streams/file_stream_error 4 | @target(erlang) 5 | import file_streams/text_encoding 6 | import gleam/bit_array 7 | import gleam/string 8 | import gleeunit 9 | import simplifile 10 | 11 | const tmp_file_name = "file_streams.test" 12 | 13 | pub fn main() { 14 | gleeunit.main() 15 | } 16 | 17 | pub fn open_missing_file_test() { 18 | assert file_stream.open_read("missing_file.txt") 19 | == Error(file_stream_error.Enoent) 20 | } 21 | 22 | pub fn open_directory_test() { 23 | assert file_stream.open_read("src") == Error(file_stream_error.Eisdir) 24 | } 25 | 26 | pub fn read_ints_and_floats_test() { 27 | assert simplifile.write_bits( 28 | tmp_file_name, 29 | bit_array.concat([ 30 | <<-100:int-size(8), 200:int-size(8)>>, 31 | // 16-bit integers 32 | << 33 | -3000:little-int-size(16), -3000:big-int-size(16), 34 | 10_000:little-int-size(16), 10_000:big-int-size(16), 35 | >>, 36 | // 32-bit integers 37 | << 38 | -300_000:little-int-size(32), -300_000:big-int-size(32), 39 | 1_000_000:little-int-size(32), 1_000_000:big-int-size(32), 40 | >>, 41 | // 64-bit integers 42 | << 43 | 9_007_199_254_740_991:little-int-size(64), 44 | 9_007_199_254_740_991:big-int-size(64), 45 | >>, 46 | // 32-bit floats 47 | << 48 | 1.5:little-float-size(32), 1.5:big-float-size(32), 49 | 2.5:little-float-size(64), 2.5:big-float-size(64), 50 | >>, 51 | // 64-bit floats 52 | << 53 | 1.0:little-float-size(64), 2.0:little-float-size(64), 54 | 3.0:little-float-size(64), 55 | >>, 56 | ]), 57 | ) 58 | == Ok(Nil) 59 | 60 | let assert Ok(stream) = 61 | file_stream.open(tmp_file_name, [file_open_mode.Read, file_open_mode.Raw]) 62 | 63 | assert file_stream.read_int8(stream) == Ok(-100) 64 | 65 | assert file_stream.read_uint8(stream) == Ok(200) 66 | 67 | assert file_stream.read_int16_le(stream) == Ok(-3000) 68 | assert file_stream.read_int16_be(stream) == Ok(-3000) 69 | 70 | assert file_stream.read_uint16_le(stream) == Ok(10_000) 71 | assert file_stream.read_uint16_be(stream) == Ok(10_000) 72 | 73 | assert file_stream.read_int32_le(stream) == Ok(-300_000) 74 | assert file_stream.read_int32_be(stream) == Ok(-300_000) 75 | 76 | assert file_stream.read_uint32_le(stream) == Ok(1_000_000) 77 | assert file_stream.read_uint32_be(stream) == Ok(1_000_000) 78 | 79 | assert file_stream.read_uint64_le(stream) == Ok(9_007_199_254_740_991) 80 | assert file_stream.read_uint64_be(stream) == Ok(9_007_199_254_740_991) 81 | 82 | assert file_stream.read_float32_le(stream) == Ok(1.5) 83 | assert file_stream.read_float32_be(stream) == Ok(1.5) 84 | 85 | assert file_stream.read_float64_le(stream) == Ok(2.5) 86 | assert file_stream.read_float64_be(stream) == Ok(2.5) 87 | 88 | assert file_stream.read_list(stream, file_stream.read_float64_le, 2) 89 | == Ok([1.0, 2.0]) 90 | 91 | assert file_stream.position(stream, file_stream.BeginningOfFile(83)) == Ok(83) 92 | assert file_stream.read_bytes_exact(stream, 0) == Ok(<<>>) 93 | 94 | assert file_stream.close(stream) == Ok(Nil) 95 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 96 | } 97 | 98 | pub fn read_bytes_exact_test() { 99 | assert simplifile.write(tmp_file_name, "Test") == Ok(Nil) 100 | 101 | let assert Ok(stream) = file_stream.open_read(tmp_file_name) 102 | 103 | assert file_stream.read_bytes_exact(stream, 2) == Ok(<<"Te":utf8>>) 104 | assert file_stream.read_bytes_exact(stream, 3) == Error(file_stream_error.Eof) 105 | 106 | assert file_stream.close(stream) == Ok(Nil) 107 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 108 | } 109 | 110 | pub fn read_remaining_bytes_test() { 111 | assert simplifile.write(tmp_file_name, string.repeat("Test", 50_000)) 112 | == Ok(Nil) 113 | 114 | let assert Ok(stream) = file_stream.open_read(tmp_file_name) 115 | let assert Ok(_) = file_stream.read_bytes_exact(stream, 100_000) 116 | 117 | let assert Ok(remaining_bytes) = file_stream.read_remaining_bytes(stream) 118 | 119 | assert bit_array.to_string(remaining_bytes) 120 | == Ok(string.repeat("Test", 25_000)) 121 | 122 | assert file_stream.close(stream) == Ok(Nil) 123 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 124 | } 125 | 126 | pub fn position_test() { 127 | assert simplifile.write(tmp_file_name, "Test1234") == Ok(Nil) 128 | 129 | let assert Ok(stream) = file_stream.open_read(tmp_file_name) 130 | 131 | assert file_stream.read_bytes_exact(stream, 2) == Ok(<<"Te":utf8>>) 132 | 133 | assert file_stream.position(stream, file_stream.CurrentLocation(-2)) == Ok(0) 134 | assert file_stream.position(stream, file_stream.CurrentLocation(-2)) 135 | == Error(file_stream_error.Einval) 136 | assert file_stream.read_bytes_exact(stream, 2) == Ok(<<"Te":utf8>>) 137 | 138 | assert file_stream.position(stream, file_stream.BeginningOfFile(4)) == Ok(4) 139 | assert file_stream.read_bytes_exact(stream, 4) == Ok(<<"1234":utf8>>) 140 | 141 | assert file_stream.position(stream, file_stream.EndOfFile(-2)) == Ok(6) 142 | assert file_stream.read_bytes_exact(stream, 2) == Ok(<<"34":utf8>>) 143 | 144 | assert file_stream.position(stream, file_stream.EndOfFile(10)) == Ok(18) 145 | assert file_stream.read_bytes_exact(stream, 1) == Error(file_stream_error.Eof) 146 | 147 | assert file_stream.position(stream, file_stream.BeginningOfFile(-100)) 148 | == Error(file_stream_error.Einval) 149 | assert file_stream.position(stream, file_stream.CurrentLocation(-100)) 150 | == Error(file_stream_error.Einval) 151 | assert file_stream.position(stream, file_stream.BeginningOfFile(6)) == Ok(6) 152 | assert file_stream.read_bytes_exact(stream, 2) == Ok(<<"34":utf8>>) 153 | 154 | assert file_stream.close(stream) == Ok(Nil) 155 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 156 | } 157 | 158 | /// Test reading and writing in the same file stream. 159 | /// 160 | pub fn read_write_test() { 161 | let assert Ok(stream) = 162 | file_stream.open(tmp_file_name, [ 163 | file_open_mode.Read, 164 | file_open_mode.Write, 165 | file_open_mode.Raw, 166 | ]) 167 | 168 | assert file_stream.write_bytes(stream, <<"Test1234":utf8>>) == Ok(Nil) 169 | 170 | assert file_stream.position(stream, file_stream.CurrentLocation(-4)) == Ok(4) 171 | assert file_stream.read_bytes(stream, 4) == Ok(<<"1234":utf8>>) 172 | assert file_stream.write_bytes(stream, <<"5678":utf8>>) == Ok(Nil) 173 | assert file_stream.position(stream, file_stream.BeginningOfFile(14)) == Ok(14) 174 | assert file_stream.write_bytes(stream, <<"9":utf8>>) == Ok(Nil) 175 | 176 | assert file_stream.close(stream) == Ok(Nil) 177 | 178 | assert simplifile.read(tmp_file_name) == Ok("Test12345678\u{0}\u{0}9") 179 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 180 | } 181 | 182 | pub fn append_test() { 183 | assert simplifile.write(tmp_file_name, "Test1234") == Ok(Nil) 184 | 185 | let assert Ok(stream) = 186 | file_stream.open(tmp_file_name, [ 187 | file_open_mode.Append, 188 | file_open_mode.Read, 189 | file_open_mode.Write, 190 | file_open_mode.Raw, 191 | ]) 192 | 193 | assert file_stream.write_bytes(stream, <<"5678">>) == Ok(Nil) 194 | assert file_stream.position(stream, file_stream.BeginningOfFile(0)) == Ok(0) 195 | assert file_stream.read_bytes(stream, 4) == Ok(<<"Test">>) 196 | assert file_stream.write_bytes(stream, <<"9">>) == Ok(Nil) 197 | assert file_stream.close(stream) == Ok(Nil) 198 | 199 | assert simplifile.read(tmp_file_name) == Ok("Test123456789") 200 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 201 | } 202 | 203 | @target(erlang) 204 | pub fn read_line_read_chars_test() { 205 | let assert Ok(stream) = file_stream.open_write(tmp_file_name) 206 | 207 | assert file_stream.write_chars(stream, "Hello\nBoo 👻!\n1🦑234\nLast") 208 | == Ok(Nil) 209 | assert file_stream.close(stream) == Ok(Nil) 210 | 211 | let assert Ok(stream) = file_stream.open_read(tmp_file_name) 212 | 213 | assert file_stream.read_line(stream) == Ok("Hello\n") 214 | assert file_stream.read_line(stream) == Ok("Boo 👻!\n") 215 | assert file_stream.read_chars(stream, 1) == Error(file_stream_error.Enotsup) 216 | assert file_stream.close(stream) == Ok(Nil) 217 | 218 | let assert Ok(stream) = 219 | file_stream.open_read_text(tmp_file_name, text_encoding.Unicode) 220 | 221 | assert file_stream.read_line(stream) == Ok("Hello\n") 222 | assert file_stream.read_line(stream) == Ok("Boo 👻!\n") 223 | assert file_stream.read_chars(stream, 1) == Ok("1") 224 | assert file_stream.read_chars(stream, 2) == Ok("🦑2") 225 | assert file_stream.read_line(stream) == Ok("34\n") 226 | assert file_stream.read_chars(stream, 5) == Ok("Last") 227 | assert file_stream.read_line(stream) == Error(file_stream_error.Eof) 228 | 229 | assert file_stream.close(stream) == Ok(Nil) 230 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 231 | } 232 | 233 | @target(erlang) 234 | pub fn read_invalid_utf8_test() { 235 | let invalid_utf8_bytes = <<0xC3, 0x28>> 236 | 237 | assert simplifile.write_bits(tmp_file_name, invalid_utf8_bytes) == Ok(Nil) 238 | 239 | let assert Ok(stream) = file_stream.open_read(tmp_file_name) 240 | 241 | assert file_stream.read_line(stream) 242 | == Error(file_stream_error.InvalidUnicode) 243 | assert file_stream.close(stream) == Ok(Nil) 244 | 245 | let assert Ok(stream) = 246 | file_stream.open_read_text(tmp_file_name, text_encoding.Unicode) 247 | 248 | assert file_stream.read_line(stream) 249 | == Error(file_stream_error.NoTranslation( 250 | text_encoding.Unicode, 251 | text_encoding.Unicode, 252 | )) 253 | 254 | assert file_stream.close(stream) == Ok(Nil) 255 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 256 | } 257 | 258 | @target(erlang) 259 | pub fn read_latin1_test() { 260 | assert simplifile.write_bits(tmp_file_name, <<0xC3, 0xD4>>) == Ok(Nil) 261 | 262 | let assert Ok(stream) = 263 | file_stream.open_read_text(tmp_file_name, text_encoding.Latin1) 264 | 265 | assert file_stream.read_bytes(stream, 2) == Ok(<<0xC3, 0xD4>>) 266 | assert file_stream.position(stream, file_stream.BeginningOfFile(0)) == Ok(0) 267 | assert file_stream.read_chars(stream, 1) == Ok("Ã") 268 | assert file_stream.read_line(stream) == Ok("Ô") 269 | 270 | assert file_stream.close(stream) == Ok(Nil) 271 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 272 | } 273 | 274 | @target(erlang) 275 | pub fn write_latin1_test() { 276 | let assert Ok(stream) = 277 | file_stream.open_write_text(tmp_file_name, text_encoding.Latin1) 278 | 279 | assert file_stream.write_chars(stream, "ÃÔ") == Ok(Nil) 280 | assert file_stream.close(stream) == Ok(Nil) 281 | assert simplifile.read_bits(tmp_file_name) == Ok(<<0xC3, 0xD4>>) 282 | 283 | let assert Ok(stream) = 284 | file_stream.open_write_text(tmp_file_name, text_encoding.Latin1) 285 | 286 | assert file_stream.write_chars(stream, "日本") 287 | == Error(file_stream_error.NoTranslation( 288 | text_encoding.Unicode, 289 | text_encoding.Latin1, 290 | )) 291 | 292 | assert file_stream.close(stream) == Ok(Nil) 293 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 294 | } 295 | 296 | @target(erlang) 297 | pub fn read_utf16le_test() { 298 | assert simplifile.write_bits(tmp_file_name, << 299 | 0xE5, 0x65, 0x2C, 0x67, 0x9E, 0x8A, 300 | >>) 301 | == Ok(Nil) 302 | 303 | let assert Ok(stream) = 304 | file_stream.open_read_text( 305 | tmp_file_name, 306 | text_encoding.Utf16(text_encoding.Little), 307 | ) 308 | 309 | assert file_stream.read_chars(stream, 2) == Ok("日本") 310 | assert file_stream.read_line(stream) == Ok("語") 311 | 312 | assert file_stream.close(stream) == Ok(Nil) 313 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 314 | } 315 | 316 | @target(erlang) 317 | pub fn write_utf16le_test() { 318 | let assert Ok(stream) = 319 | file_stream.open_write_text( 320 | tmp_file_name, 321 | text_encoding.Utf16(text_encoding.Little), 322 | ) 323 | 324 | assert file_stream.write_chars(stream, "日本語") == Ok(Nil) 325 | assert file_stream.close(stream) == Ok(Nil) 326 | assert simplifile.read_bits(tmp_file_name) 327 | == Ok(<<0xE5, 0x65, 0x2C, 0x67, 0x9E, 0x8A>>) 328 | 329 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 330 | } 331 | 332 | @target(erlang) 333 | pub fn read_utf32be_test() { 334 | assert simplifile.write_bits(tmp_file_name, << 335 | 0x00, 0x01, 0x03, 0x48, 0xFF, 0xFF, 0xFF, 0xFF, 336 | >>) 337 | == Ok(Nil) 338 | 339 | let assert Ok(stream) = 340 | file_stream.open_read_text( 341 | tmp_file_name, 342 | text_encoding.Utf32(text_encoding.Big), 343 | ) 344 | 345 | assert file_stream.read_chars(stream, 1) == Ok("𐍈") 346 | assert file_stream.read_chars(stream, 1) 347 | == Error(file_stream_error.InvalidUnicode) 348 | 349 | assert file_stream.close(stream) == Ok(Nil) 350 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 351 | } 352 | 353 | @target(erlang) 354 | pub fn write_utf32be_test() { 355 | let assert Ok(stream) = 356 | file_stream.open_write_text( 357 | tmp_file_name, 358 | text_encoding.Utf32(text_encoding.Big), 359 | ) 360 | 361 | assert file_stream.write_chars(stream, "𐍈") == Ok(Nil) 362 | assert file_stream.close(stream) == Ok(Nil) 363 | 364 | assert simplifile.read_bits(tmp_file_name) == Ok(<<0x00, 0x01, 0x03, 0x48>>) 365 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 366 | } 367 | 368 | @target(erlang) 369 | pub fn set_encoding_test() { 370 | let assert Ok(stream) = 371 | file_stream.open_write_text(tmp_file_name, text_encoding.Latin1) 372 | 373 | assert file_stream.write_chars(stream, "ÃÔ") == Ok(Nil) 374 | 375 | let assert Ok(stream) = 376 | file_stream.set_encoding(stream, text_encoding.Unicode) 377 | 378 | assert file_stream.write_chars(stream, "👻") == Ok(Nil) 379 | assert file_stream.close(stream) == Ok(Nil) 380 | 381 | assert simplifile.read_bits(tmp_file_name) 382 | == Ok(<<0xC3, 0xD4, 0xF0, 0x9F, 0x91, 0xBB>>) 383 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 384 | } 385 | 386 | pub fn write_partial_bytes_test() { 387 | let assert Ok(stream) = file_stream.open_write(tmp_file_name) 388 | 389 | assert file_stream.write_bytes(stream, <<"A", 0:7>>) 390 | == Error(file_stream_error.Einval) 391 | 392 | assert file_stream.close(stream) == Ok(Nil) 393 | assert simplifile.delete(tmp_file_name) == Ok(Nil) 394 | } 395 | --------------------------------------------------------------------------------