├── .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 | [](https://hex.pm/packages/file_streams)
8 | [](https://hexdocs.pm/file_streams/)
9 | 
10 | 
11 | [](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 |
--------------------------------------------------------------------------------