retroforth/example/tuporo.retro
crc 0e7db6dab9 remove deprecate words
FossilOrigin-Name: d0ecbf2ac180314d604e62ca9462f118303c09084af969105d4dcecde8704535
2023-03-29 00:56:00 +00:00

244 lines
6.5 KiB
Forth

#!/usr/bin/env retro
# tūporo: a gopher-based block store for retro
## Overview
Traditional Forths often provided simple editors oriented around blocks.
A standard block is a single unit of text, with 16 lines of 64 symbols
per line, or 1024 characters. While far less common now, blocks are
still useful, and RETRO has had some support for them in most of its
incarnations.
With my recent interest in Gopher, I've decided that my next take on
blocks will use Gopher.
So the basic goal of this is to provide a Gopher server capable of
transfer and update of blocks, and also a means of browsing the blocks
via Gopher. As with all of my recent servers, this will run under `tui`,
`tcpserver` or `inetd`.
## Configuration
The basic configuration settings are the number of blocks (`MAX-BLOCKS`)
and the path to the block file (including the file name). This is also
where the server URL (or IP) and port are set.
~~~
:BLOCKS '/home/crc/blocks ;
:SERVER 'forthworks.com ;
#8081 'PORT const
#200 'MAX-BLOCKS const
~~~
## Block I/O
First up are a scratch variable (`FID`) to hold the file ID for use with
reads/writes, and a safe buffer to store the currently loaded block. I
will use the `buffer:` namespace for interacting with the block, so it
needs to be one cell longer than the actual data length to account for
the final ASCII NUL terminator.
~~~
'FID var
'Block d:create #1025 allot
~~~
`block:locate` moves the index in the blockfile to the actual starting
point for a particular block.
~~~
:block:locate (n-)
#1024 * @FID file:seek ;
~~~
`block:copy` copies the data for the current block into the `Block`
buffer.
~~~
:block:copy (-)
#1024 [ @FID file:read buffer:add ] times ;
~~~
The top level `block:get` word sets the current buffer to `Block`, then
loads the block file and copies the requested block into the buffer. It
returns the address of the `Block` buffer.
~~~
:block:get (n-s)
&Block buffer:set
BLOCKS file:R file:open !FID
block:locate block:copy
@FID file:close &Block ;
~~~
`block:set` writes a string into a block. The string *can* be longer
than a block, in which case it writes to subsequent blocks.
~~~
:block:set (sn-)
BLOCKS file:R+ file:open !FID
block:locate dup s:length [ fetch-next @FID file:write ] times drop
@FID file:close ;
~~~
## Browsing
To be able to browse the blocks, we first need a means of displaying a
top level index (returned when the Gopher client sends a request as an
empty selector string).
I'll use `generate-index` for this. A Gopher directory line looks like:
<type><description>\t<selector>\t<server>\t<port>
The type of interest here is:
0 plain text
I define `generate-entry` to make a line for a block. It takes a
description and selector and uses the SERVER and PORT variables to
construct the line.
~~~
:generate-entry (ss-)
SERVER PORT 'abcd 'dcba reorder '0%s\t%s\t%s\t%n s:format s:put nl ;
~~~
With this it's easy to define `generate-index` using a loop to make a
usable directory index listing all blocks.
~~~
:generate-index (-)
#0 MAX-BLOCKS
[ dup n:to-string over '/%n s:format generate-entry n:inc ] times
drop ;
~~~
Displaying a block as plain text is very easy. Using `block:get` to fetch
the data, it's just two loops (one for each line, one for each charaacter)
displaying the characters and newlines as needed.
~~~
:display-block (n-)
block:get #16 [ #64 [ fetch-next c:put ] times nl ] times drop ;
~~~
## ...
## Gopher Protocol
Tūporo decides what to do based on the selectors passed to it. These are
what I will recognize:
/ directory index of all blocks
/nnnn block #nnnn (as formatted text data)
/r/nnnn block #nnnn (as raw text data)
/s/nnnn/text change block #nnnn to specified raw text data
I have a `Selector` buffer for storing the selector the user passes in.
This is sized to be big enough for the incoming block data (if using /s)
with room to spare.
~~~
'Selector d:create #4096 allot
~~~
The `prefix` word returns the first two characters of the selector. This
will be enough to identify what type of request we are dealing with.
~~~
:prefix (-s)
&Selector #0 #2 s:substr ;
~~~
`raw-block` returns a raw, unformatted block as text data. This will
correspond to /r/nnnn selectors.
~~~
:raw-block (-)
&Selector #3 + s:chop block:get s:put ;
~~~
`set-block` updates a block with new text. This selector takes a form:
/s/block#/text
It's probably *not* a good idea to leave this exposed on a public
server as there is no means provided of restricting writes using it.
~~~
:set-block (-)
&Selector #3 +
$/ s:split/char s:to-number swap n:inc s:chop swap block:set ;
~~~
And `handle-block` uses `display-block` to return a formatted text block
when browsing.
~~~
:handle-block (-)
&Selector n:inc s:chop s:to-number display-block ;
~~~
The top level `handle` word decides how to handle each selector using the
results of `prefix`. Selectors that don't match up to one of the handled
ones just return a directory listing.
~~~
:handle
prefix
'/r [ raw-block ] s:case
'/s [ set-block ] s:case
'/0 [ handle-block ] s:case
'/1 [ handle-block ] s:case
'/2 [ handle-block ] s:case
'/3 [ handle-block ] s:case
'/4 [ handle-block ] s:case
'/5 [ handle-block ] s:case
'/6 [ handle-block ] s:case
'/7 [ handle-block ] s:case
'/8 [ handle-block ] s:case
'/9 [ handle-block ] s:case
drop generate-index ;
~~~
And finally, a quick bit from Atua to read in the selector and pass it
to `handle`
~~~
:eol? (c-f)
[ ASCII:CR eq? ] [ ASCII:LF eq? ] [ ASCII:HT eq? ] tri or or ;
:s:get (a-)
buffer:set
[ s:get dup buffer:add eol? not ] while ;
&Selector s:get handle
~~~
## Future Direction
It'd probably be a good idea to add some authentication so unknown users
can't write changes to the block store.
Other than that, it's a simple, clean system for exposing a blockfile via
Gopher.
## Legalities
Copyright (c) 2017, Charles Childers
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.