# `F3.IO.Local`

Local filesystem source.

`open/2` stats the path and keeps that size for later reads. `pread/3`
never asks the operating system for more bytes than remain in that size.
A missing path is `:not_found`. `close/1` is idempotent.

`:file_system` replaces `F3.IO.FileSystem.OS` for tests.

# `t`

```elixir
@type t() :: %F3.IO.Local{
  closed: boolean(),
  fd: term(),
  file_system: module(),
  path: Path.t(),
  size: non_neg_integer()
}
```

Local source.

`path` is the opened path. `fd` is the `:file` descriptor. `size` is the
size from `stat`. `file_system` is the `F3.IO.FileSystem` module. `closed`
is true after `close/1`.

# `close`

Closes the descriptor. A second close returns the same handle.

## Examples

    iex> {:ok, state} = F3.IO.Local.open("test/fixtures/small.f3")
    iex> {:ok, closed} = F3.IO.Local.close(state)
    iex> closed.closed
    true

# `open`

```elixir
@spec open(Path.t(), keyword()) :: {:ok, t()} | {:error, F3.Error.t()}
```

Opens `path` for positioned reads.

`file_system:` replaces `F3.IO.FileSystem.OS`. A missing path is
`:not_found`. A path that is not a regular file is `:invalid_argument`.
This does not raise for a missing path.

## Examples

    iex> {:ok, state} = F3.IO.Local.open("test/fixtures/small.f3")
    iex> state.size
    4168

    iex> {:error, error} = F3.IO.Local.open("missing-file.f3")
    iex> error.category
    :not_found

# `pread`

Reads at most `length` bytes at `offset`, and never past the size from
`open/2`. A start at or past the end is `{:ok, ""}`.

## Examples

    iex> {:ok, state} = F3.IO.Local.open("test/fixtures/small.f3")
    iex> F3.IO.Local.pread(state, 4166, 2)
    {:ok, "F3"}
    iex> :ok = elem(F3.IO.Local.close(state), 0)
    :ok

# `size`

Size recorded by `open/2`. A closed handle is `:closed`.

## Examples

    iex> {:ok, state} = F3.IO.Local.open("test/fixtures/small.f3")
    iex> F3.IO.Local.size(state)
    {:ok, 4168}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
