Added tests and test data, refactored where needed (#4)

Added tests and closes #2 .
- [x] Added tests, including defensive tests
- [X] Code coverage is 87.8%
- [X] Added testdata
- [X] Refactored where needed

Co-authored-by: Nicolas Herry <beastieboy@beastieboy.net>
Reviewed-on: http://www.beastieboy.net:3000/git/beastieboy/marmotte/pulls/4
Co-authored-by: beastieboy <beastieboy@beastieboy.net>
Co-committed-by: beastieboy <beastieboy@beastieboy.net>
pull/9/head
beastieboy 1 year ago
parent c1fb7f09f8
commit 22ac3810bd
  1. 1
      go.mod
  2. 2
      go.sum
  3. 44
      gopher/context.go
  4. 136
      gopher/context_test.go
  5. 61
      gopher/files.go
  6. 76
      gopher/files_test.go
  7. 37
      gopher/predicates.go
  8. 219
      gopher/predicates_test.go
  9. 12
      gopher/request.go
  10. 111
      gopher/request_test.go
  11. 10
      gopher/response.go
  12. 86
      gopher/response_test.go
  13. 86
      gopher/server.go
  14. 125
      gopher/server_test.go
  15. 6
      gopher/transformers.go
  16. 179
      gopher/transformers_test.go
  17. 1
      testdata/about-me
  18. 1
      testdata/about-server
  19. 597
      testdata/attic/Goblin_Market
  20. BIN
      testdata/attic/audio/Bubbles-SoundBible.com-810959520.mp3
  21. BIN
      testdata/attic/blob
  22. BIN
      testdata/attic/cat
  23. BIN
      testdata/attic/gopher.docx
  24. BIN
      testdata/attic/pictures/hamtori.gif
  25. BIN
      testdata/attic/pictures/starry_night.jpg
  26. BIN
      testdata/attic/rfc1436.txt.pdf
  27. 5
      testdata/gophermap
  28. 1
      testdata/writings/gophermap
  29. 788
      testdata/writings/rime_of_the_ancient_mariner

@ -4,6 +4,7 @@ go 1.19
require (
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/rs/zerolog v1.28.0 // indirect

@ -2,6 +2,8 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=

@ -1,6 +1,8 @@
package gopher
import (
"fmt"
"errors"
"regexp"
"github.com/rs/zerolog/log"
@ -18,22 +20,32 @@ type Context struct {
}
func (c *Context) AddRequestTransformer(t RequestTransformer) ([]RequestTransformer, error) {
c.RequestTransformers = append(c.RequestTransformers, t)
return c.RequestTransformers, nil
if t.Transformer != nil && t.Predicate != nil {
c.RequestTransformers = append(c.RequestTransformers, t)
return c.RequestTransformers, nil
} else {
return c.RequestTransformers, errors.New(fmt.Sprintf("Invalid RequestTransformer instance, both the Transformer and the Predicate must not be nil. Transformer == nil: %t, Predicate == nil: %t",
t.Transformer == nil,
t.Predicate == nil))
}
}
func (c *Context) AddRequestTransformerFor(s string, f RequestTransformerFunc) ([]RequestTransformer, error) {
_, e := regexp.Compile(s)
if e != nil {
log.Err(e).
Str("string", s).
Send()
return c.RequestTransformers, e
if f == nil {
return c.RequestTransformers, errors.New("Invalid RequestTransformerFunc instance, cannot be nil")
} else {
t := RequestTransformer{Transformer: f,
Predicate: FullPathMatchPredicate,
_, e := regexp.Compile(s)
if e != nil {
log.Err(e).
Str("string", s).
Send()
return c.RequestTransformers, e
} else {
t := RequestTransformer{Transformer: f,
Predicate: FullPathMatchPredicateFor(s),
}
return c.AddRequestTransformer(t)
}
return c.AddRequestTransformer(t)
}
}
@ -46,9 +58,15 @@ func (c *Context) DefaultResponseTransformers() []ResponseTransformer {
Predicate: FileTypePlainTextPredicate }
binary := ResponseTransformer {
Transformer: BinaryFileTransformer,
Predicate: FileTypeBinaryPredicate }
Predicate: FileTypeBinaryPredicate }
image := ResponseTransformer {
Transformer: BinaryFileTransformer,
Predicate: FileTypeImagePredicate }
audio := ResponseTransformer {
Transformer: BinaryFileTransformer,
Predicate: FileTypeAudioPredicate }
c.ResponseTransformers = []ResponseTransformer{ gophermap, textfile, binary }
c.ResponseTransformers = []ResponseTransformer{ gophermap, textfile, binary, image, audio }
return c.ResponseTransformers
}

@ -0,0 +1,136 @@
package gopher
import (
"testing"
"os"
// "github.com/google/go-cmp/cmp"
)
func DummyRequestTransformer(c *Context, rq *Request, rs Response, err error) (*Context, *Request, Response, error) {
return c, rq, rs, err
}
func DummyRequestPredicate(c Context, rq Request, rs Response, err error) bool {
return true
}
func TestAddRequestTransformer(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
rt := RequestTransformer { Transformer: DummyRequestTransformer,
Predicate: DummyRequestPredicate,
}
expected := len(c.RequestTransformers)
expected++
c.AddRequestTransformer(rt)
got := len(c.RequestTransformers)
if got != expected {
t.Errorf("Unexpected number of RequestTransformer instances in context after adding a valid transformer: expected %d, got %d",
expected, got)
}
rt = RequestTransformer { Transformer: DummyRequestTransformer,
Predicate: nil,
}
_, err := c.AddRequestTransformer(rt)
got = len(c.RequestTransformers)
if got != expected {
t.Errorf("Unexpected number of RequestTransformer instances in context after adding an invalid transformer: expected %d, got %d",
expected, got)
}
if err == nil {
t.Error("Expected an error after adding an invalid Transformer to a context, didn't get any")
}
}
func TestAddRequestTransformerFor(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p := "/"
expected := len(c.RequestTransformers)
expected++
_, err := c.AddRequestTransformerFor(p, DummyRequestTransformer)
if err != nil {
t.Errorf("Unexpected error when adding a valid Transformer to a context for a valid path: Path=%s, error=%s", p, err)
}
got := len(c.RequestTransformers)
if got != expected {
t.Errorf("Unexpected number of RequestTransformer instances in context after adding a valid transformer: expected %d, got %d", expected, got)
}
_, err = c.AddRequestTransformerFor(p, nil)
got = len(c.RequestTransformers)
if got != expected {
t.Errorf("Unexpected number of RequestTransformer instances in context after adding an invalid transformer: expected %d, got %d",
expected, got)
}
if err == nil {
t.Error("Expected an error after adding an invalid Transformer to a context, didn't get any")
}
expected = len(c.RequestTransformers)
p = ".*aa["
gotTransformers, err := c.AddRequestTransformerFor(p, DummyRequestTransformer)
got = len(gotTransformers)
if err == nil {
t.Errorf("Expected an error after using an invalid pattern to add a RequestTransformer to a context, got none, for pattern %s", p)
}
if got != expected {
t.Errorf("Unexpected number of RequestTransformer instances in context after adding an invalid path %s, expected %d, got %d", p, expected, got)
}
}
func TestDefaultResponseTransformers(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
c.DefaultResponseTransformers()
files := map[string]string{
"gophermapfile":"/gophermap",
"textfile": "/attic/Goblin_Market",
"binaryfile": "/attic/cat",
"imagefile": "/attic/pictures/hamtori.gif",
"audiofile": "/attic/audio/Bubbles-SoundBible.com-810959520.mp3",
}
for ft, fp := range files {
rq, _ := NewRequest(c, fp)
rs := NewResponse(c, *rq)
found := false
var err error = nil
for _, t := range c.ResponseTransformers {
if t.Predicate(c, *rq, *rs, err) {
found = true
}
}
if found != true {
t.Errorf("No predicate returned true for base file type %s, with path %s", ft, fp)
}
}
}

@ -12,45 +12,47 @@ import (
)
// non-idiomatic :(
// func compose(transformers ...transformer) transformer {
// return func(c Context, rq Request, rs Response, e error) (Context, Request, Response, error) {
// t:= transformers[0]
// ts := transformers[1:]
// if len(transformers) == 1 {
// return t(c, rq, rs, e)
// }
// return t(compose(ts...)(c, rq, rs, e))
// }
// }
func FileType(p string) (ResponseType, error) {
mimegopher := map[*regexp.Regexp]ResponseType{
regexp.MustCompile("text/.*"): PlainText,
regexp.MustCompile("audio/*"): Audio,
regexp.MustCompile("image/.*"): Image,
regexp.MustCompile(".*/pdf"): PDF,
regexp.MustCompile(".*/msword"): Doc,
regexp.MustCompile("(.*/msword)|(application/vnd.openxmlformats-officedocument.wordprocessingml.document)"): Doc,
}
log.Debug().Str("p", p).Send()
mtype, err := mimetype.DetectFile(p)
fi, err := os.Stat(p)
if err != nil {
log.Error().Err(err).
Str("Path", p).
Msg("Error while detecting a file type")
log.Error().
Err(err).
Msg("Received an error when trying to stat a full path")
return Binary, err
}
if fi.IsDir() {
return Directory, nil
} else {
mtype, err := mimetype.DetectFile(p)
log.Debug().
Str("Filetype", mtype.String()).
Str("Extension", mtype.Extension())
for m, t := range mimegopher {
if m.MatchString(p) {
return t, nil
if err != nil {
log.Error().Err(err).
Str("Path", p).
Msg("Error while detecting a file type")
}
}
for m, t := range mimegopher {
if m.MatchString(mtype.String()) {
log.Debug().
Str("Filetype", mtype.String()).
Str("Extension", mtype.Extension()).
Str("regexp", m.String()).
Send()
return t, nil
}
}
}
// type not found, default to binary
return Binary, nil
}
@ -66,7 +68,6 @@ func GopherMapLine(c Context, r string, f string, t ResponseType) (string, error
return s + f + "\t" + r + "/" + f + "\t" + c.Host + "\t" + c.Port + "\r\n", nil
}
func GopherMap(c Context, r string) ([]string, error) {
g := c.Root + r + "/gophermap"
if _, err := os.Stat(g); errors.Is(err, os.ErrNotExist) {
@ -87,8 +88,7 @@ func GopherMap(c Context, r string) ([]string, error) {
l, _ := GopherMapLine(c, r, entry.Name(), Directory)
lines = append(lines, l)
} else {
mtype, _ := mimetype.DetectFile(c.Root + r + "/" + entry.Name())
t, _ := FileType(mtype.String())
t, _ := FileType(c.Root + r + "/" + entry.Name())
l, _ := GopherMapLine(c, r, entry.Name(), t)
lines = append(lines, l)
}
@ -106,8 +106,9 @@ func GopherMap(c Context, r string) ([]string, error) {
}
func ReadTextFile(p string) ([]string, error) {
f, err := os.Open(p)
lines := []string{}
f, err := os.Open(p)
defer f.Close()
if err != nil {
return []string{}, fmt.Errorf("Error reading the file %s: %s", p, err.Error())

@ -0,0 +1,76 @@
package gopher
import (
"testing"
"os"
)
func TestFileType(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
files := map[string]ResponseType {c.Root + "/attic/Goblin_Market": PlainText,
c.Root + "/attic/pictures/starry_night.jpg": Image,
c.Root + "/attic/pictures/hamtori.gif": Image,
c.Root + "/attic/audio/Bubbles-SoundBible.com-810959520.mp3": Audio,
c.Root + "/": Directory,
}
for f, expected := range files {
got, err := FileType(f)
if err != nil {
t.Errorf("Error determining the file type of %s: %s", f, err)
}
if got != expected {
t.Errorf("Unexpected file type for %s: expected %d, got %d", f, int(expected), int(got))
}
}
p := "/attic/non-existent"
_, err := FileType(p)
if err == nil {
t.Errorf("Expected an error when calling FileType with a non-existent path %s, didn'r receive any", p)
}
}
func TestGopherMap(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
lines, err := GopherMap(c, "")
expected := true
got := len(lines) > 0
if err != nil {
t.Errorf("Unexpected error when calling GopherMap with an empty path: %s", err)
}
if got != expected {
t.Error("Unexpected empty lines when calling GopherMap with an empty path")
}
p := "/some/non/existant/path"
lines, err = GopherMap(c, p)
expectedlines := 0
gotlines := len(lines)
if err == nil {
t.Errorf("Expected receiving an error when calling GopherMap with an invalid path: %s, err: %s", p, err)
}
if got != expected {
t.Errorf("Unexpected non-empty lines when calling GopherMap with an empty path, expected %d, got %d", expectedlines, gotlines)
}
}

@ -8,17 +8,34 @@ import (
type TransformerPredicate func(Context, Request, Response, error) bool
func FullPathMatchPredicate(c Context, rq Request, rs Response, e error) bool {
r, e := regexp.Compile(rq.FullPath)
if e == nil && r.MatchString(rq.FullPath) {
return true
} else if e != nil {
log.Err(e).
Str("fullpath", rq.FullPath).
Send()
}
// func FullPathMatchPredicate(c Context, rq Request, rs Response, e error) bool {
// r, e := regexp.Compile(rq.FullPath)
// if e == nil && r.MatchString(rq.FullPath) {
// return true
// } else if e != nil {
// log.Err(e).
// Str("fullpath", rq.FullPath).
// Send()
// }
// return false
// }
return false
func FullPathMatchPredicateFor(p string) func(Context, Request, Response, error)(bool) {
return func(c Context, rq Request, rs Response, e error) bool {
r, e := regexp.Compile(p)
if e == nil && r.MatchString(rq.FullPath) {
return true
} else if e != nil {
log.Err(e).
Str("fullpath", rq.FullPath).
Str("pattern", p).
Send()
}
return false
}
}
func GopherMapPredicate(c Context, rq Request, rs Response, e error) bool {

@ -0,0 +1,219 @@
package gopher
import (
"testing"
"os"
)
func TestFullPathMatchPredicateFor(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
rq, _ := NewRequest(c, "/attic/Goblin_Market")
rs := NewResponse(c, *rq)
p := "/attic"
pred := FullPathMatchPredicateFor(p)
expected := true
got := pred(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from a generated FullPathMatchPredicate for path %s, expected %t, got %t", p, expected, got)
}
rq, _ = NewRequest(c, "/writings")
rs = NewResponse(c, *rq)
expected = false
got = pred(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from a generated FullPathMatchPredicate for path %s, expected %t, got %t", p, expected, got)
}
}
func TestGopherMapPredicate(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= ""
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
expected := true
got := GopherMapPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from GopherMapPredicate for path %s, expected %t, got %t", p, expected, got)
}
p = "/attic"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
expected = true
got = GopherMapPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from GopherMapPredicate for path %s, expected %t, got %t", p, expected, got)
}
}
func TestFileTypeBinaryPredicate(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/cat"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
expected := true
got := FileTypeBinaryPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeBinaryPredicate for path %s, expected %t, got %t", p, expected, got)
}
p = "/attic/Goblin_Market"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
expected = false
got = FileTypeBinaryPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeBinaryPredicate for path %s, expected %t, got %t", p, expected, got)
}
}
func TestFileTypePlainTextPredicate(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/Goblin_Market"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
expected := true
got := FileTypePlainTextPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypePlainTextPredicate for path %s, expected %t, got %t", p, expected, got)
}
p = "/attic/"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
expected = false
got = FileTypeBinaryPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypePlainTextPredicate for path %s, expected %t, got %t", p, expected, got)
}
}
func TestFileTypeAudioPredicate(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/audio/Bubbles-SoundBible.com-810959520.mp3"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
expected := true
got := FileTypeAudioPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeAudioPredicate for path %s, expected %t, got %t", p, expected, got)
}
p = "/attic/Goblin_Market"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
expected = false
got = FileTypeAudioPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeAudioPredicate for path %s, expected %t, got %t", p, expected, got)
}
}
func TestFileTypeImagePredicate(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/pictures/hamtori.gif"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
expected := true
got := FileTypeImagePredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeImagePredicate for path %s, expected %t, got %t", p, expected, got)
}
p = "/attic/Goblin_Market"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
expected = false
got = FileTypeImagePredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeImagePredicate for path %s, expected %t, got %t", p, expected, got)
}
}
func TestFileTypePDFPredicate(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/rfc1436.txt.pdf"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
expected := true
got := FileTypePDFPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypePDFPredicate for path %s, expected %t, got %t", p, expected, got)
}
p = "/attic/Goblin_Market"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
expected = false
got = FileTypePDFPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypePDFPredicate for path %s, expected %t, got %t", p, expected, got)
}
}
func TestFileTypeDocPredicate(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/gopher.docx"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
expected := true
got := FileTypeDocPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeDocPredicate for path %s, expected %t, got %t", p, expected, got)
}
p = "/attic/Goblin_Market"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
expected = false
got = FileTypePDFPredicate(c, *rq, *rs, nil)
if got != expected {
t.Errorf("Unexpected return value from FileTypeDocPredicate for path %s, expected %t, got %t", p, expected, got)
}
}

@ -13,6 +13,7 @@ type RequestType int
const (
Map RequestType = iota
File
NotImplemented
)
type Request struct {
@ -37,13 +38,22 @@ func NewRequest(c Context, s string) (*Request, error) {
} else if Selector.MatchString(s) {
r.Path = s
r.FullPath = c.Root + s
fi, _ := os.Stat(r.FullPath)
fi, err := os.Stat(r.FullPath)
if err != nil {
log.Error().
Err(err).
Msg("Received an error when trying to stat a full path")
return &r, err
}
if fi.IsDir() {
r.Type = Map
} else {
r.Type = File
}
} else {
// by default, consider that what is not a directory or a path to a file is 'NotImplemented'
// later: this can be a function, etc.
r.Type = NotImplemented
return &r, fmt.Errorf("Unrecognised request: '%s'", s)
}

@ -0,0 +1,111 @@
package gopher
import (
"testing"
"os"
"github.com/google/go-cmp/cmp"
)
func TestNewRequest(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
// root gophermap
rq, err := NewRequest(c, "")
if err != nil {
t.Error("Expected no error, got: ", err)
}
expectedType := Map
if rq.Type != expectedType {
t.Errorf("Unexpected type for Request, expected %d, got %d", expectedType, rq.Type)
}
expectedPath := "/"
if rq.Path != expectedPath {
t.Errorf("Unexpected path for Request, expected %s got %s", expectedPath, rq.Path)
}
rq, err = NewRequest(c, "/")
if err != nil {
t.Error("Expected no error, got: ", err)
}
expectedType = Map
if rq.Type != expectedType {
t.Errorf("Unexpected type for Request, expected %d, got %d", expectedType, rq.Type)
}
expectedPath = "/"
if rq.Path != expectedPath {
t.Errorf("Unexpected path for Request, expected %s got %s", expectedPath, rq.Path)
}
rq, err = NewRequest(c, "/attic")
if err != nil {
t.Error("Expected no error, got: ", err)
}
expectedType = Map
if rq.Type != expectedType {
t.Errorf("Unexpected type for Request, expected %d, got %d", expectedType, rq.Type)
}
expectedPath = "/attic"
if rq.Path != expectedPath {
t.Errorf("Unexpected path for Request, expected %s got %s", expectedPath, rq.Path)
}
rq, err = NewRequest(c, "/attic/Goblin_Market")
if err != nil {
t.Error("Expected no error, got: ", err)
}
expectedType = File
if rq.Type != expectedType {
t.Errorf("Unexpected type for Request, expected %d, got %d", expectedType, rq.Type)
}
expectedPath = "/attic/Goblin_Market"
if rq.Path != expectedPath {
t.Errorf("Unexpected path for Request, expected %s got %s", expectedPath, rq.Path)
}
rq, err = NewRequest(c, "NO-LEADING-SLASH")
if err == nil {
t.Error("Expected to receive an error, didn't get any for a non-implemented request")
}
expectedRq := Request{ Type: NotImplemented }
if ! cmp.Equal(&expectedRq, rq) {
t.Errorf("Unexpected Request instance returned by NewRequest for an non-implemented request: %s", cmp.Diff(&expectedRq, rq))
}
p := "/non-existent"
rq, err = NewRequest(c, p)
if err == nil {
t.Errorf("Expected to receive an error, didn't get any for a non-existent request %s", p)
}
expectedRq = Request{ Path: p, FullPath: c.Root + p}
if ! cmp.Equal(&expectedRq, rq) {
t.Errorf("Unexpected Request instance returned by NewRequest for an non-existent request %s: %s", p, cmp.Diff(&expectedRq, rq))
}
}

@ -42,7 +42,7 @@ func NewResponse(c Context, rq Request) *Response {
ContentBinary: []byte{},
ContentString: "",
}
if rq.Type == Map {
r.Type = PlainText
} else {
@ -52,12 +52,16 @@ func NewResponse(c Context, rq Request) *Response {
Err(err).
Str("FullPath", rq.FullPath).
Msg("Error detecting a file type")
return &r
}
log.Debug().
Str("Filetype:", mtype.String()).
Str("Extension", mtype.Extension())
Str("Extension", mtype.Extension()).
Send()
t, err := FileType(rq.FullPath)
log.Debug().Int("t", int(t)).Send()
if err != nil {
log.Error().
Err(err).

@ -0,0 +1,86 @@
package gopher
import (
"testing"
"os"
)
func TestNewResponse(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
// root gophermap
rq, _ := NewRequest(c, "")
rs := NewResponse(c, *rq)
var expectedType ResponseType = PlainText
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response for %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
rq, _ = NewRequest(c, "/")
rs = NewResponse(c, *rq)
expectedType = PlainText
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
rq, _ = NewRequest(c, "/attic")
rs = NewResponse(c, *rq)
expectedType = PlainText
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response for %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
rq, _ = NewRequest(c, "/attic/Goblin_Market")
rs = NewResponse(c, *rq)
expectedType = PlainText
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response for %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
rq, _ = NewRequest(c, "/attic/pictures/starry_night.jpg")
rs = NewResponse(c, *rq)
expectedType = Image
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response for %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
rq, _ = NewRequest(c, "/attic/pictures/hamtori.gif")
rs = NewResponse(c, *rq)
expectedType = Image
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response for %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
rq, _ = NewRequest(c, "/attic/audio/Bubbles-SoundBible.com-810959520.mp3")
rs = NewResponse(c, *rq)
expectedType = Audio
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response for %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
rq, _ = NewRequest(c, "/attic/blob")
rs = NewResponse(c, *rq)
expectedType = Binary
if rs.Type != expectedType {
t.Errorf("Unexpected type for Response for %s, expected %d, got %d", rq.Path, expectedType, rs.Type)
}
}

@ -2,26 +2,68 @@ package gopher
import (
"bufio"
"fmt"
"net"
"io"
"errors"
"github.com/rs/zerolog/log"
)
// ugly, needs refactoring
func write(ctx *Context, rq Request, rs *Response, w *bufio.Writer, c net.Conn) error {
func write(ctx *Context, rq Request, rs *Response, w io.Writer) error {
if rs.Type == PlainText {
for _, line := range rs.ContentText {
fmt.Fprint(w, line)
w.Write([]byte(line))
}
fmt.Fprint(w, ".\r\n")
w.Flush()
w.Write([]byte(".\r\n"))
} else {
c.Write(rs.ContentBinary)
w.Write(rs.ContentBinary)
}
return nil
}
func ParseRequest(c *Context, r io.Reader) (*Request, error) {
if r == nil {
return &Request{}, errors.New("nil io.Reader instance passed to ParseRequest")
}
s := bufio.NewScanner(bufio.NewReader(r))
s.Scan()
if e := s.Err(); e != nil {
log.Error().
Err(e).
Msg("Error reading input")
return &Request{}, e
}
rq, err := NewRequest(*c, s.Text())
if err != nil {
log.Error().
Err(err).
Msg("Got an error when creating a new Request: ")
}
log.Debug().
Str("Request", rq.Path).
Send()
return rq, err
}
func Transform(c *Context, rq Request, rs *Response, err error) []ResponseTransformer {
transformers := []ResponseTransformer{}
for _, t := range *&c.ResponseTransformers {
if t.Predicate(*c, rq, *rs, err) {
t.Transformer(c, rq, rs, err)
transformers = append(transformers, t)
}
}
return transformers
}
func Serve(ctx *Context) {
l, _ := net.Listen(ctx.TransportProtocol, ":" + ctx.Port)
defer l.Close()
@ -34,41 +76,19 @@ func Serve(ctx *Context) {
Err(err).
Msg("Error connecting a client")
} else {
s := bufio.NewScanner(bufio.NewReader(c))
s.Scan()
w := bufio.NewWriter(c)
rq, err := NewRequest(*ctx, s.Text())
if err != nil {
log.Error().
Err(err).
Msg("Got an error when creating a new Request: ")
}
rq, err := ParseRequest(ctx, c)
log.Debug().
Str("Request", rq.Path).
Send()
rs := NewResponse(*ctx, *rq)
for _, t := range *&ctx.ResponseTransformers {
if t.Predicate(*ctx, *rq, *rs, err) {
ctx, _, rs, err = t.Transformer(ctx, *rq, rs, err)
}
}
Transform(ctx, *rq, rs, err)
err = write(ctx, *rq, rs, c)
err = write(ctx, *rq, rs, w, c)
if err != nil {
log.Error().
Err(err).
Msg("An error occurred while writing the data on the socket:")
}
if err := s.Err(); err != nil {
log.Error().
Err(err).
Msg("Error reading input")
}
} // else
c.Close() // always close connection

@ -0,0 +1,125 @@
package gopher
import (
"os"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
type fakeWriter struct { Buffer []byte}
func (w *fakeWriter) Write(b []byte) (n int, err error) {
w.Buffer = append(w.Buffer, b...)
return len(w.Buffer), nil
}
type brokenReader struct { Text string }
func (r *brokenReader) Read(p []byte) (n int, err error) {
p = []byte(r.Text)
return len(p), nil
}
func TestWrite(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/Goblin_Market"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
w := fakeWriter { Buffer: []byte{} }
var err error = nil
TextFileTransformer(&c, *rq, rs, err)
err = write(&c, *rq, rs, &w)
expected := true
got := len(w.Buffer) > 0
if got != expected {
t.Errorf("Unexpected empty buffer in fakeWriter instance after a valid write call for text on path %s", p)
}
p = "/attic/cat"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
w = fakeWriter { Buffer: []byte{} }
BinaryFileTransformer(&c, *rq, rs, err)
err = write(&c, *rq, rs, &w)
expected = true
got = len(w.Buffer) > 0
if got != expected {
t.Errorf("Unexpected empty buffer in fakeWriter instance after a valid write call for binary on path %s", p)
}
}
func TestParseRequest(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/Goblin_Market\r\n"
r := strings.NewReader(p)
rq, err := ParseRequest(&c, r)
if err != nil {
t.Errorf("Unexpected error when parsing the request string %s: %s", p, err)
}
expected := File
got := rq.Type
if got != expected {
t.Errorf("Unexpected file type in Request instance when parsing the request string %s, expected %d, got %d", p, expected, got)
}
expectedRq := Request{}
gotRq, err := ParseRequest(&c, nil)
if err == nil {
t.Error("Expected receiving an error when calling ParseRequest with a nil reader, didn't get any")
}
if ! cmp.Equal(&expectedRq, gotRq) {
t.Errorf("Unexpected Request instance after parsing a request with a nil reader, diff: %s", cmp.Diff(&expectedRq, gotRq))
}
b := brokenReader{}
expectedRq = Request{}
gotRq, err = ParseRequest(&c, &b)
if err == nil {
t.Error("Expected receiving an error when calling ParseRequest with a brokenReader, didn't get any")
}
if ! cmp.Equal(&expectedRq, gotRq) {
t.Errorf("Unexpected Request instance after parsing a request with a brokenReader, diff: %s", cmp.Diff(&expectedRq, gotRq))
}
}
func TestTransform(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
c.DefaultResponseTransformers()
p:= "/attic/Goblin_Market"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
var err error = nil
transformers := Transform(&c, *rq, rs, err)
expected := true
got := len(transformers) > 0
if got != expected {
t.Errorf("Unexpected empty list of applied transformers after a call to Transform for path %s", p)
}
}

@ -26,6 +26,7 @@ func GopherMapTransformer(c *Context, rq Request, rs *Response, e error) (*Conte
log.Err(err).
Str("path", rq.Path).
Send()
return c, rq, rs, err
} else {
rs.ContentText = append(rs.ContentText, s...)
}
@ -34,11 +35,12 @@ func GopherMapTransformer(c *Context, rq Request, rs *Response, e error) (*Conte
}
func TextFileTransformer(c *Context, rq Request, rs *Response, e error) (*Context, Request, *Response, error) {
s, err := ReadTextFile(rq.Path)
s, err := ReadTextFile(rq.FullPath)
if err != nil {
log.Err(err).
Str("path", rq.Path).
Str("path", rq.FullPath).
Send()
return c, rq, rs, err
} else {
rs.ContentText = append(rs.ContentText, s...)
}

@ -0,0 +1,179 @@
package gopher
import (
"testing"
"os"
)
func TestGopherMapTransformer(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
var err error = nil
GopherMapTransformer(&c, *rq, rs, err)
expected := true
got := len(rs.ContentText) > 0
if got != expected {
t.Errorf("Unexpected empty ContentText in Response instance after a valid GopherMapTransformer call on path %s", p)
}
expected = false
got = len(rs.ContentBinary) > 0
if got != expected {
t.Errorf("Unexpected non-empty ContentBinary in Response instance after a valid GopherMapTransformer call on path %s", p)
}
p = "/attic/non/existent/path"
rq, _ = NewRequest(c, "")
// craft an invalid request
rq.FullPath = c.Root + p
rq.Path = p
rs = NewResponse(c, *rq)
err = nil
_, _, _, err = GopherMapTransformer(&c, *rq, rs, err)
expectedlines := 0
gotlines := len(rs.ContentText)
if err == nil {
t.Errorf("Expected receiving an error when calling GopherMapTransformer with a non-existent directory %s", p)
}
if gotlines != expectedlines {
t.Errorf("Unexpected number of lines when calling GopherMapTransformer with a non-existent directory %s, expected %d, got %d", p, expectedlines, gotlines)
}
}
func TestTextFileTransformer(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/Goblin_Market"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
var err error = nil
TextFileTransformer(&c, *rq, rs, err)
expected := true
got := len(rs.ContentText) > 0
if got != expected {
t.Errorf("Unexpected empty ContentText in Response instance after a valid TextFileTransformer call on path %s", p)
}
expected = false
got = len(rs.ContentBinary) > 0
if got != expected {
t.Errorf("Unexpected non-empty ContentBinary in Response instance after a valid TextFileTransformer call on path %s", p)
}
p = "/attic/Goblin_Market"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
// craft a broken request
rq.FullPath = c.Root + "/non-existent"
rq.Path = "/non-existent"
err = nil
expectedlines := len(rs.ContentText)
_, _, _, err = TextFileTransformer(&c, *rq, rs, err)
gotlines := len(rs.ContentText)
if err == nil {
t.Errorf("Expected receiving an error after calling TextFileTransformer with a broken path %s in the request, didn't receive any", p)
}
if got != expected {
t.Errorf("Unexpected number of lines for ContentText in Response instance after an invalid TextFileTransformer call on path %s, expected %d, got %d", p, expectedlines, gotlines)
}
}
func TestBinaryFileTransformer(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata" }
p:= "/attic/cat"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
var err error = nil
BinaryFileTransformer(&c, *rq, rs, err)
expected := true
got := len(rs.ContentBinary) > 0
if got != expected {
t.Errorf("Unexpected empty ContentBinary in Response instance after a valid BinaryFileTransformer call on path %s", p)
}
expected = false
got = len(rs.ContentText) > 0
if got != expected {
t.Errorf("Unexpected non-empty ContentText in Response instance after a valid BinaryFileTransformer call on path %s", p)
}
p = "/attic/cat"
rq, _ = NewRequest(c, p)
rs = NewResponse(c, *rq)
// craft a broken request
rq.FullPath = c.Root + "/non-existent"
rq.Path = "/non-existent"
err = nil
expectedsize := len(rs.ContentBinary)
_, _, _, err = BinaryFileTransformer(&c, *rq, rs, err)
gotsize := len(rs.ContentBinary)
if err == nil {
t.Errorf("Expected an error after calling BinaryFileTransformer with a broken path %s in the request, didn't get any", p)
}
if gotsize != expectedsize {
t.Errorf("Unexpected size of ContentBinary in Response instance after an invalid BinaryFileTransformer call on path %s, expected %d, got %d", p, expectedsize, gotsize)
}
}
func TestHeaderTransformer(t *testing.T) {
d, _ := os.Getwd()
c := Context { Root: d + "/../testdata", Header: []string{"powered", "by", "marmotte"} }
c.ResponseTransformers = append(c.ResponseTransformers, ResponseTransformer{
Transformer: HeaderTransformer,
Predicate: GopherMapPredicate })
p:= "/attic/"
rq, _ := NewRequest(c, p)
rs := NewResponse(c, *rq)
var err error = nil
HeaderTransformer(&c, *rq, rs, err)
for i, h := range c.Header {
expected := "i" + h + "\r\n"
got := rs.ContentText[i]
if got != expected {
t.Errorf("Unexpected line in ContentText of a Response instance, expected %s, got %s (index %d)", expected, got, i)
}
}
expected := false
got := len(rs.ContentBinary) > 0
if got != expected {
t.Errorf("Unexpected non-empty ContentBinary in Response instance after a valid HeaderTransformer call on path %s", p)
}
}

1
testdata/about-me vendored

@ -0,0 +1 @@
I am beastieboy and I write funny programs.

@ -0,0 +1 @@
This server is an experimental Gopher server written in Go.

@ -0,0 +1,597 @@
Goblin Market
By Christina Rossetti
Morning and evening
Maids heard the goblins cry:
“Come buy our orchard fruits,
Come buy, come buy:
Apples and quinces,
Lemons and oranges,
Plump unpeck’d cherries,
Melons and raspberries,
Bloom-down-cheek’d peaches,
Swart-headed mulberries,
Wild free-born cranberries,
Crab-apples, dewberries,
Pine-apples, blackberries,
Apricots, strawberries;—
All ripe together
In summer weather,—
Morns that pass by,
Fair eves that fly;
Come buy, come buy:
Our grapes fresh from the vine,
Pomegranates full and fine,
Dates and sharp bullaces,
Rare pears and greengages,
Damsons and bilberries,
Taste them and try:
Currants and gooseberries,
Bright-fire-like barberries,
Figs to fill your mouth,
Citrons from the South,
Sweet to tongue and sound to eye;
Come buy, come buy.”
Evening by evening
Among the brookside rushes,
Laura bow’d her head to hear,
Lizzie veil’d her blushes:
Crouching close together
In the cooling weather,
With clasping arms and cautioning lips,
With tingling cheeks and finger tips.
“Lie close,” Laura said,
Pricking up her golden head:
“We must not look at goblin men,
We must not buy their fruits:
Who knows upon what soil they fed
Their hungry thirsty roots?”
“Come buy,” call the goblins
Hobbling down the glen.
“Oh,” cried Lizzie, “Laura, Laura,
You should not peep at goblin men.”
Lizzie cover’d up her eyes,
Cover’d close lest they should look;
Laura rear’d her glossy head,
And whisper’d like the restless brook:
“Look, Lizzie, look, Lizzie,
Down the glen tramp little men.
One hauls a basket,
One bears a plate,
One lugs a golden dish
Of many pounds weight.
How fair the vine must grow
Whose grapes are so luscious;
How warm the wind must blow
Through those fruit bushes.”
“No,” said Lizzie, “No, no, no;
Their offers should not charm us,
Their evil gifts would harm us.”
She thrust a dimpled finger
In each ear, shut eyes and ran:
Curious Laura chose to linger
Wondering at each merchant man.
One had a cat’s face,
One whisk’d a tail,
One tramp’d at a rat’s pace,
One crawl’d like a snail,
One like a wombat prowl’d obtuse and furry,
One like a ratel tumbled hurry skurry.
She heard a voice like voice of doves
Cooing all together:
They sounded kind and full of loves
In the pleasant weather.
Laura stretch’d her gleaming neck
Like a rush-imbedded swan,
Like a lily from the beck,
Like a moonlit poplar branch,
Like a vessel at the launch
When its last restraint is gone.
Backwards up the mossy glen
Turn’d and troop’d the goblin men,
With their shrill repeated cry,
“Come buy, come buy.”
When they reach’d where Laura was
They stood stock still upon the moss,
Leering at each other,
Brother with queer brother;
Signalling each other,
Brother with sly brother.
One set his basket down,
One rear’d his plate;
One began to weave a crown
Of tendrils, leaves, and rough nuts brown
(Men sell not such in any town);
One heav’d the golden weight
Of dish and fruit to offer her:
“Come buy, come buy,” was still their cry.
Laura stared but did not stir,
Long’d but had no money:
The whisk-tail’d merchant bade her taste
In tones as smooth as honey,
The cat-faced purr’d,
The rat-faced spoke a word
Of welcome, and the snail-paced even was heard;
One parrot-voiced and jolly
Cried “Pretty Goblin” still for “Pretty Polly;”—
One whistled like a bird.
But sweet-tooth Laura spoke in haste:
“Good folk, I have no coin;
To take were to purloin:
I have no copper in my purse,
I have no silver either,
And all my gold is on the furze
That shakes in windy weather
Above the rusty heather.”
“You have much gold upon your head,”
They answer’d all together:
“Buy from us with a golden curl.”
She clipp’d a precious golden lock,
She dropp’d a tear more rare than pearl,
Then suck’d their fruit globes fair or red:
Sweeter than honey from the rock,
Stronger than man-rejoicing wine,
Clearer than water flow’d that juice;
She never tasted such before,
How should it cloy with length of use?
She suck’d and suck’d and suck’d the more
Fruits which that unknown orchard bore;
She suck’d until her lips were sore;
Then flung the emptied rinds away
But gather’d up one kernel stone,
And knew not was it night or day
As she turn’d home alone.
Lizzie met her at the gate
Full of wise upbraidings:
“Dear, you should not stay so late,
Twilight is not good for maidens;
Should not loiter in the glen
In the haunts of goblin men.
Do you not remember Jeanie,
How she met them in the moonlight,
Took their gifts both choice and many,
Ate their fruits and wore their flowers
Pluck’d from bowers
Where summer ripens at all hours?
But ever in the noonlight
She pined and pined away;
Sought them by night and day,
Found them no more, but dwindled and grew grey;
Then fell with the first snow,
While to this day no grass will grow
Where she lies low:
I planted daisies there a year ago
That never blow.
You should not loiter so.”
“Nay, hush,” said Laura:
“Nay, hush, my sister:
I ate and ate my fill,
Yet my mouth waters still;
To-morrow night I will
Buy more;” and kiss’d her:
“Have done with sorrow;
I’ll bring you plums to-morrow
Fresh on their mother twigs,
Cherries worth getting;
You cannot think what figs
My teeth have met in,
What melons icy-cold
Piled on a dish of gold
Too huge for me to hold,
What peaches with a velvet nap,
Pellucid grapes without one seed:
Odorous indeed must be the mead
Whereon they grow, and pure the wave they drink
With lilies at the brink,
And sugar-sweet their sap.”
Golden head by golden head,
Like two pigeons in one nest
Folded in each other’s wings,
They lay down in their curtain’d bed:
Like two blossoms on one stem,
Like two flakes of new-fall’n snow,
Like two wands of ivory
Tipp’d with gold for awful kings.
Moon and stars gaz’d in at them,
Wind sang to them lullaby,
Lumbering owls forbore to fly,
Not a bat flapp’d to and fro
Round their rest:
Cheek to cheek and breast to breast
Lock’d together in one nest.
Early in the morning
When the first cock crow’d his warning,
Neat like bees, as sweet and busy,
Laura rose with Lizzie:
Fetch’d in honey, milk’d the cows,
Air’d and set to rights the house,
Kneaded cakes of whitest wheat,
Cakes for dainty mouths to eat,
Next churn’d butter, whipp’d up cream,
Fed their poultry, sat and sew’d;
Talk’d as modest maidens should:
Lizzie with an open heart,
Laura in an absent dream,
One content, one sick in part;
One warbling for the mere bright day’s delight,
One longing for the night.
At length slow evening came:
They went with pitchers to the reedy brook;
Lizzie most placid in her look,
Laura most like a leaping flame.
They drew the gurgling water from its deep;
Lizzie pluck’d purple and rich golden flags,
Then turning homeward said: “The sunset flushes
Those furthest loftiest crags;
Come, Laura, not another maiden lags.
No wilful squirrel wags,
The beasts and birds are fast asleep.”
But Laura loiter’d still among the rushes
And said the bank was steep.
And said the hour was early still
The dew not fall’n, the wind not chill;
Listening ever, but not catching
The customary cry,
“Come buy, come buy,”
With its iterated jingle
Of sugar-baited words:
Not for all her watching
Once discerning even one goblin
Racing, whisking, tumbling, hobbling;
Let alone the herds
That used to tramp along the glen,
In groups or single,
Of brisk fruit-merchant men.
Till Lizzie urged, “O Laura, come;
I hear the fruit-call but I dare not look:
You should not loiter longer at this brook:
Come with me home.
The stars rise, the moon bends her arc,
Each glowworm winks her spark,
Let us get home before the night grows dark:
For clouds may gather
Though this is summer weather,
Put out the lights and drench us through;
Then if we lost our way what should we do?”
Laura turn’d cold as stone
To find her sister heard that cry alone,
That goblin cry,
“Come buy our fruits, come buy.”
Must she then buy no more such dainty fruit?
Must she no more such succous pasture find,
Gone deaf and blind?
Her tree of life droop’d from the root:
She said not one word in her heart’s sore ache;
But peering thro’ the dimness, nought discerning,
Trudg’d home, her pitcher dripping all the way;
So crept to bed, and lay
Silent till Lizzie slept;
Then sat up in a passionate yearning,
And gnash’d her teeth for baulk’d desire, and wept
As if her heart would break.
Day after day, night after night,
Laura kept watch in vain
In sullen silence of exceeding pain.
She never caught again the goblin cry:
“Come buy, come buy;”—
She never spied the goblin men
Hawking their fruits along the glen:
But when the noon wax’d bright
Her hair grew thin and grey;
She dwindled, as the fair full moon doth turn
To swift decay and burn
Her fire away.
One day remembering her kernel-stone
She set it by a wall that faced the south;
Dew’d it with tears, hoped for a root,
Watch’d for a waxing shoot,
But there came none;
It never saw the sun,
It never felt the trickling moisture run:
While with sunk eyes and faded mouth
She dream’d of melons, as a traveller sees
False waves in desert drouth
With shade of leaf-crown’d trees,
And burns the thirstier in the sandful breeze.
She no more swept the house,
Tended the fowls or cows,
Fetch’d honey, kneaded cakes of wheat,
Brought water from the brook:
But sat down listless in the chimney-nook
And would not eat.
Tender Lizzie could not bear
To watch her sister’s cankerous care
Yet not to share.
She night and morning
Caught the goblins’ cry:
“Come buy our orchard fruits,
Come buy, come buy;”—
Beside the brook, along the glen,
She heard the tramp of goblin men,
The yoke and stir
Poor Laura could not hear;
Long’d to buy fruit to comfort her,
But fear’d to pay too dear.
She thought of Jeanie in her grave,
Who should have been a bride;
But who for joys brides hope to have
Fell sick and died
In her gay prime,
In earliest winter time
With the first glazing rime,
With the first snow-fall of crisp winter time.
Till Laura dwindling
Seem’d knocking at Death’s door:
Then Lizzie weigh’d no more
Better and worse;
But put a silver penny in her purse,
Kiss’d Laura, cross’d the heath with clumps of furze
At twilight, halted by the brook:
And for the first time in her life
Began to listen and look.
Laugh’d every goblin
When they spied her peeping:
Came towards her hobbling,
Flying, running, leaping,
Puffing and blowing,
Chuckling, clapping, crowing,
Clucking and gobbling,
Mopping and mowing,
Full of airs and graces,
Pulling wry faces,
Demure grimaces,
Cat-like and rat-like,
Ratel- and wombat-like,
Snail-paced in a hurry,
Parrot-voiced and whistler,
Helter skelter, hurry skurry,
Chattering like magpies,
Fluttering like pigeons,
Gliding like fishes,—
Hugg’d her and kiss’d her:
Squeez’d and caress’d her:
Stretch’d up their dishes,
Panniers, and plates:
“Look at our apples
Russet and dun,
Bob at our cherries,
Bite at our peaches,
Citrons and dates,
Grapes for the asking,
Pears red with basking
Out in the sun,
Plums on their twigs;
Pluck them and suck them,
Pomegranates, figs.”—
“Good folk,” said Lizzie,
Mindful of Jeanie:
“Give me much and many: —
Held out her apron,
Toss’d them her penny.
“Nay, take a seat with us,
Honour and eat with us,”
They answer’d grinning:
“Our feast is but beginning.
Night yet is early,
Warm and dew-pearly,
Wakeful and starry:
Such fruits as these
No man can carry:
Half their bloom would fly,
Half their dew would dry,
Half their flavour would pass by.
Sit down and feast with us,
Be welcome guest with us,
Cheer you and rest with us.”—
“Thank you,” said Lizzie: “But one waits
At home alone for me:
So without further parleying,
If you will not sell me any
Of your fruits though much and many,
Give me back my silver penny
I toss’d you for a fee.”—
They began to scratch their pates,
No longer wagging, purring,
But visibly demurring,
Grunting and snarling.
One call’d her proud,
Cross-grain’d, uncivil;
Their tones wax’d loud,
Their looks were evil.
Lashing their tails
They trod and hustled her,
Elbow’d and jostled her,
Claw’d with their nails,
Barking, mewing, hissing, mocking,
Tore her gown and soil’d her stocking,
Twitch’d her hair out by the roots,
Stamp’d upon her tender feet,
Held her hands and squeez’d their fruits
Against her mouth to make her eat.
White and golden Lizzie stood,
Like a lily in a flood,—
Like a rock of blue-vein’d stone
Lash’d by tides obstreperously,—
Like a beacon left alone
In a hoary roaring sea,
Sending up a golden fire,—
Like a fruit-crown’d orange-tree
White with blossoms honey-sweet
Sore beset by wasp and bee,—
Like a royal virgin town
Topp’d with gilded dome and spire
Close beleaguer’d by a fleet
Mad to tug her standard down.
One may lead a horse to water,
Twenty cannot make him drink.
Though the goblins cuff’d and caught her,
Coax’d and fought her,
Bullied and besought her,
Scratch’d her, pinch’d her black as ink,
Kick’d and knock’d her,
Maul’d and mock’d her,
Lizzie utter’d not a word;
Would not open lip from lip
Lest they should cram a mouthful in:
But laugh’d in heart to feel the drip
Of juice that syrupp’d all her face,
And lodg’d in dimples of her chin,
And streak’d her neck which quaked like curd.
At last the evil people,
Worn out by her resistance,
Flung back her penny, kick’d their fruit
Along whichever road they took,
Not leaving root or stone or shoot;
Some writh’d into the ground,
Some div’d into the brook
With ring and ripple,
Some scudded on the gale without a sound,
Some vanish’d in the distance.
In a smart, ache, tingle,
Lizzie went her way;
Knew not was it night or day;
Sprang up the bank, tore thro’ the furze,
Threaded copse and dingle,
And heard her penny jingle
Bouncing in her purse,—
Its bounce was music to her ear.
She ran and ran
As if she fear’d some goblin man
Dogg’d her with gibe or curse
Or something worse:
But not one goblin scurried after,
Nor was she prick’d by fear;
The kind heart made her windy-paced
That urged her home quite out of breath with haste
And inward laughter.
She cried, “Laura,” up the garden,
“Did you miss me?
Come and kiss me.
Never mind my bruises,
Hug me, kiss me, suck my juices
Squeez’d from goblin fruits for you,
Goblin pulp and goblin dew.
Eat me, drink me, love me;
Laura, make much of me;
For your sake I have braved the glen
And had to do with goblin merchant men.”
Laura started from her chair,
Flung her arms up in the air,
Clutch’d her hair:
“Lizzie, Lizzie, have you tasted
For my sake the fruit forbidden?
Must your light like mine be hidden,
Your young life like mine be wasted,
Undone in mine undoing,
And ruin’d in my ruin,
Thirsty, canker’d, goblin-ridden?”—
She clung about her sister,
Kiss’d and kiss’d and kiss’d her:
Tears once again
Refresh’d her shrunken eyes,
Dropping like rain
After long sultry drouth;
Shaking with aguish fear, and pain,
She kiss’d and kiss’d her with a hungry mouth.
Her lips began to scorch,
That juice was wormwood to her tongue,
She loath’d the feast:
Writhing as one possess’d she leap’d and sung,
Rent all her robe, and wrung
Her hands in lamentable haste,
And beat her breast.
Her locks stream’d like the torch
Borne by a racer at full speed,
Or like the mane of horses in their flight,
Or like an eagle when she stems the light
Straight toward the sun,
Or like a caged thing freed,
Or like a flying flag when armies run.
Swift fire spread through her veins, knock’d at her heart,
Met the fire smouldering there
And overbore its lesser flame;
She gorged on bitterness without a name:
Ah! fool, to choose such part
Of soul-consuming care!
Sense fail’d in the mortal strife:
Like the watch-tower of a town
Which an earthquake shatters down,
Like a lightning-stricken mast,
Like a wind-uprooted tree
Spun about,
Like a foam-topp’d waterspout
Cast down headlong in the sea,
She fell at last;
Pleasure past and anguish past,
Is it death or is it life?
Life out of death.
That night long Lizzie watch’d by her,
Counted her pulse’s flagging stir,
Felt for her breath,
Held water to her lips, and cool’d her face
With tears and fanning leaves:
But when the first birds chirp’d about their eaves,
And early reapers plodded to the place
Of golden sheaves,
And dew-wet grass
Bow’d in the morning winds so brisk to pass,
And new buds with new day
Open’d of cup-like lilies on the stream,
Laura awoke as from a dream,
Laugh’d in the innocent old way,
Hugg’d Lizzie but not twice or thrice;
Her gleaming locks show’d not one thread of grey,
Her breath was sweet as May
And light danced in her eyes.
Days, weeks, months, years
Afterwards, when both were wives
With children of their own;
Their mother-hearts beset with fears,
Their lives bound up in tender lives;
Laura would call the little ones
And tell them of her early prime,
Those pleasant days long gone
Of not-returning time:
Would talk about the haunted glen,
The wicked, quaint fruit-merchant men,
Their fruits like honey to the throat
But poison in the blood;
(Men sell not such in any town):
Would tell them how her sister stood
In deadly peril to do her good,
And win the fiery antidote:
Then joining hands to little hands
Would bid them cling together,
“For there is no friend like a sister
In calm or stormy weather;
To cheer one on the tedious way,
To fetch one if one goes astray,
To lift one if one totters down,
To strengthen whilst one stands.”

BIN
testdata/attic/blob vendored

Binary file not shown.

BIN
testdata/attic/cat vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

@ -0,0 +1,5 @@
iThis is the menu from the gophermap! fake (NULL) 0
1Writings /writings localhost 7070
1Attic /attic localhost 7070
0About this server /about-server localhost 7070
0About me /about-me localhost 7070

@ -0,0 +1 @@
0Rime Of the Ancient Mariner, by Samuel Taylor Coleridge /writings/rime_of_the_ancient_mariner localhost 7070

@ -0,0 +1,788 @@
The Rime of the Ancient Mariner (text of 1834)
By Samuel Taylor Coleridge
Argument
How a Ship having passed the Line was driven by storms to the cold Country towards the South Pole; and how from thence she made her course to the tropical Latitude of the Great Pacific Ocean; and of the strange things that befell; and in what manner the Ancyent Marinere came back to his own Country.
PART I
It is an ancient Mariner,
And he stoppeth one of three.
'By thy long grey beard and glittering eye,
Now wherefore stopp'st thou me?
The Bridegroom's doors are opened wide,
And I am next of kin;
The guests are met, the feast is set:
May'st hear the merry din.'
He holds him with his skinny hand,
'There was a ship,' quoth he.
'Hold off! unhand me, grey-beard loon!'
Eftsoons his hand dropt he.
He holds him with his glittering eye—
The Wedding-Guest stood still,
And listens like a three years' child:
The Mariner hath his will.
The Wedding-Guest sat on a stone:
He cannot choose but hear;
And thus spake on that ancient man,
The bright-eyed Mariner.
'The ship was cheered, the harbour cleared,
Merrily did we drop
Below the kirk, below the hill,
Below the lighthouse top.
The Sun came up upon the left,
Out of the sea came he!
And he shone bright, and on the right
Went down into the sea.
Higher and higher every day,
Till over the mast at noon—'
The Wedding-Guest here beat his breast,
For he heard the loud bassoon.
The bride hath paced into the hall,
Red as a rose is she;
Nodding their heads before her goes
The merry minstrelsy.
The Wedding-Guest he beat his breast,
Yet he cannot choose but hear;
And thus spake on that ancient man,
The bright-eyed Mariner.
And now the STORM-BLAST came, and he
Was tyrannous and strong:
He struck with his o'ertaking wings,
And chased us south along.
With sloping masts and dipping prow,
As who pursued with yell and blow
Still treads the shadow of his foe,
And forward bends his head,
The ship drove fast, loud roared the blast,
And southward aye we fled.
And now there came both mist and snow,
And it grew wondrous cold:
And ice, mast-high, came floating by,
As green as emerald.
And through the drifts the snowy clifts
Did send a dismal sheen:
Nor shapes of men nor beasts we ken—
The ice was all between.
The ice was here, the ice was there,
The ice was all around:
It cracked and growled, and roared and howled,
Like noises in a swound!
At length did cross an Albatross,
Thorough the fog it came;
As if it had been a Christian soul,
We hailed it in God's name.
It ate the food it ne'er had eat,
And round and round it flew.
The ice did split with a thunder-fit;
The helmsman steered us through!
And a good south wind sprung up behind;
The Albatross did follow,
And every day, for food or play,
Came to the mariner's hollo!
In mist or cloud, on mast or shroud,
It perched for vespers nine;
Whiles all the night, through fog-smoke white,
Glimmered the white Moon-shine.'
'God save thee, ancient Mariner!
From the fiends, that plague thee thus!—
Why look'st thou so?'—With my cross-bow
I shot the ALBATROSS.
PART II
The Sun now rose upon the right:
Out of the sea came he,
Still hid in mist, and on the left
Went down into the sea.
And the good south wind still blew behind,
But no sweet bird did follow,
Nor any day for food or play
Came to the mariner's hollo!
And I had done a hellish thing,
And it would work 'em woe:
For all averred, I had killed the bird
That made the breeze to blow.
Ah wretch! said they, the bird to slay,
That made the breeze to blow!
Nor dim nor red, like God's own head,
The glorious Sun uprist:
Then all averred, I had killed the bird
That brought the fog and mist.
'Twas right, said they, such birds to slay,
That bring the fog and mist.
The fair breeze blew, the white foam flew,
The furrow followed free;
We were the first that ever burst
Into that silent sea.
Down dropt the breeze, the sails dropt down,
'Twas sad as sad could be;
And we did speak only to break
The silence of the sea!
All in a hot and copper sky,
The bloody Sun, at noon,
Right up above the mast did stand,
No bigger than the Moon.
Day after day, day after day,
We stuck, nor breath nor motion;
As idle as a painted ship
Upon a painted ocean.
Water, water, every where,
And all the boards did shrink;
Water, water, every where,
Nor any drop to drink.
The very deep did rot: O Christ!
That ever this should be!
Yea, slimy things did crawl with legs
Upon the slimy sea.
About, about, in reel and rout
The death-fires danced at night;
The water, like a witch's oils,
Burnt green, and blue and white.
And some in dreams assurèd were
Of the Spirit that plagued us so;
Nine fathom deep he had followed us
From the land of mist and snow.
And every tongue, through utter drought,
Was withered at the root;
We could not speak, no more than if
We had been choked with soot.
Ah! well a-day! what evil looks
Had I from old and young!
Instead of the cross, the Albatross
About my neck was hung.
PART III
There passed a weary time. Each throat
Was parched, and glazed each eye.
A weary time! a weary time!
How glazed each weary eye,
When looking westward, I beheld
A something in the sky.
At first it seemed a little speck,
And then it seemed a mist;
It moved and moved, and took at last
A certain shape, I wist.
A speck, a mist, a shape, I wist!
And still it neared and neared:
As if it dodged a water-sprite,
It plunged and tacked and veered.
With throats unslaked, with black lips baked,
We could nor laugh nor wail;
Through utter drought all dumb we stood!
I bit my arm, I sucked the blood,
And cried, A sail! a sail!
With throats unslaked, with black lips baked,
Agape they heard me call:
Gramercy! they for joy did grin,
And all at once their breath drew in.
As they were drinking all.
See! see! (I cried) she tacks no more!
Hither to work us weal;
Without a breeze, without a tide,
She steadies with upright keel!
The western wave was all a-flame.
The day was well nigh done!
Almost upon the western wave
Rested the broad bright Sun;
When that strange shape drove suddenly
Betwixt us and the Sun.
And straight the Sun was flecked with bars,
(Heaven's Mother send us grace!)
As if through a dungeon-grate he peered
With broad and burning face.
Alas! (thought I, and my heart beat loud)
How fast she nears and nears!
Are those her sails that glance in the Sun,
Like restless gossameres?
Are those her ribs through which the Sun
Did peer, as through a grate?
And is that Woman all her crew?
Is that a DEATH? and are there two?
Is DEATH that woman's mate?
Her lips were red, her looks were free,
Her locks were yellow as gold:
Her skin was as white as leprosy,
The Night-mare LIFE-IN-DEATH was she,
Who thicks man's blood with cold.
The naked hulk alongside came,
And the twain were casting dice;
'The game is done! I've won! I've won!'
Quoth she, and whistles thrice.
The Sun's rim dips; the stars rush out;
At one stride comes the dark;
With far-heard whisper, o'er the sea,
Off shot the spectre-bark.
We listened and looked sideways up!
Fear at my heart, as at a cup,
My life-blood seemed to sip!
The stars were dim, and thick the night,
The steersman's face by his lamp gleamed white;
From the sails the dew did drip—
Till clomb above the eastern bar
The hornèd Moon, with one bright star
Within the nether tip.
One after one, by the star-dogged Moon,
Too quick for groan or sigh,
Each turned his face with a ghastly pang,
And cursed me with his eye.
Four times fifty living men,
(And I heard nor sigh nor groan)
With heavy thump, a lifeless lump,
They dropped down one by one.
The souls did from their bodies fly,—
They fled to bliss or woe!
And every soul, it passed me by,
Like the whizz of my cross-bow!
PART IV
'I fear thee, ancient Mariner!
I fear thy skinny hand!
And thou art long, and lank, and brown,
As is the ribbed sea-sand.
I fear thee and thy glittering eye,
And thy skinny hand, so brown.'—
Fear not, fear not, thou Wedding-Guest!
This body dropt not down.
Alone, alone, all, all alone,
Alone on a wide wide sea!
And never a saint took pity on
My soul in agony.
The many men, so beautiful!
And they all dead did lie:
And a thousand thousand slimy things
Lived on; and so did I.
I looked upon the rotting sea,
And drew my eyes away;
I looked upon the rotting deck,
And there the dead men lay.
I looked to heaven, and tried to pray;
But or ever a prayer had gusht,
A wicked whisper came, and made
My heart as dry as dust.
I closed my lids, and kept them close,
And the balls like pulses beat;
For the sky and the sea, and the sea and the sky
Lay dead like a load on my weary eye,
And the dead were at my feet.
The cold sweat melted from their limbs,
Nor rot nor reek did they:
The look with which they looked on me
Had never passed away.
An orphan's curse would drag to hell
A spirit from on high;
But oh! more horrible than that
Is the curse in a dead man's eye!
Seven days, seven nights, I saw that curse,
And yet I could not die.
The moving Moon went up the sky,
And no where did abide:
Softly she was going up,
And a star or two beside—
Her beams bemocked the sultry main,
Like April hoar-frost spread;
But where the ship's huge shadow lay,
The charmèd water burnt alway
A still and awful red.
Beyond the shadow of the ship,
I watched the water-snakes:
They moved in tracks of shining white,
And when they reared, the elfish light
Fell off in hoary flakes.
Within the shadow of the ship
I watched their rich attire:
Blue, glossy green, and velvet black,
They coiled and swam; and every track
Was a flash of golden fire.
O happy living things! no tongue
Their beauty might declare:
A spring of love gushed from my heart,
And I blessed them unaware:
Sure my kind saint took pity on me,
And I blessed them unaware.
The self-same moment I could pray;
And from my neck so free
The Albatross fell off, and sank
Like lead into the sea.
PART V
Oh sleep! it is a gentle thing,
Beloved from pole to pole!
To Mary Queen the praise be given!
She sent the gentle sleep from Heaven,
That slid into my soul.
The silly buckets on the deck,
That had so long remained,
I dreamt that they were filled with dew;
And when I awoke, it rained.
My lips were wet, my throat was cold,
My garments all were dank;
Sure I had drunken in my dreams,
And still my body drank.
I moved, and could not feel my limbs:
I was so light—almost
I thought that I had died in sleep,
And was a blessed ghost.
And soon I heard a roaring wind:
It did not come anear;
But with its sound it shook the sails,
That were so thin and sere.
The upper air burst into life!
And a hundred fire-flags sheen,
To and fro they were hurried about!
And to and fro, and in and out,
The wan stars danced between.
And the coming wind did roar more loud,
And the sails did sigh like sedge,
And the rain poured down from one black cloud;
The Moon was at its edge.
The thick black cloud was cleft, and still
The Moon was at its side:
Like waters shot from some high crag,
The lightning fell with never a jag,
A river steep and wide.
The loud wind never reached the ship,
Yet now the ship moved on!
Beneath the lightning and the Moon
The dead men gave a groan.
They groaned, they stirred, they all uprose,
Nor spake, nor moved their eyes;
It had been strange, even in a dream,
To have seen those dead men rise.
The helmsman steered, the ship moved on;
Yet never a breeze up-blew;
The mariners all 'gan work the ropes,
Where they were wont to do;
They raised their limbs like lifeless tools—
We were a ghastly crew.
The body of my brother's son
Stood by me, knee to knee:
The body and I pulled at one rope,
But he said nought to me.
'I fear thee, ancient Mariner!'
Be calm, thou Wedding-Guest!
'Twas not those souls that fled in pain,
Which to their corses came again,
But a troop of spirits blest:
For when it dawned—they dropped their arms,
And clustered round the mast;
Sweet sounds rose slowly through their mouths,
And from their bodies passed.
Around, around, flew each sweet sound,
Then darted to the Sun;
Slowly the sounds came back again,
Now mixed, now one by one.
Sometimes a-dropping from the sky
I heard the sky-lark sing;
Sometimes all little birds that are,
How they seemed to fill the sea and air
With their sweet jargoning!
And now 'twas like all instruments,
Now like a lonely flute;
And now it is an angel's song,
That makes the heavens be mute.
It ceased; yet still the sails made on
A pleasant noise till noon,
A noise like of a hidden brook
In the leafy month of June,
That to the sleeping woods all night
Singeth a quiet tune.
Till noon we quietly sailed on,
Yet never a breeze did breathe:
Slowly and smoothly went the ship,
Moved onward from beneath.
Under the keel nine fathom deep,
From the land of mist and snow,
The spirit slid: and it was he
That made the ship to go.
The sails at noon left off their tune,
And the ship stood still also.
The Sun, right up above the mast,
Had fixed her to the ocean:
But in a minute she 'gan stir,
With a short uneasy motion—
Backwards and forwards half her length
With a short uneasy motion.
Then like a pawing horse let go,
She made a sudden bound:
It flung the blood into my head,
And I fell down in a swound.
How long in that same fit I lay,
I have not to declare;
But ere my living life returned,
I heard and in my soul discerned
Two voices in the air.
'Is it he?' quoth one, 'Is this the man?
By him who died on cross,
With his cruel bow he laid full low
The harmless Albatross.
The spirit who bideth by himself
In the land of mist and snow,
He loved the bird that loved the man
Who shot him with his bow.'
The other was a softer voice,
As soft as honey-dew:
Quoth he, 'The man hath penance done,
And penance more will do.'
PART VI
First Voice
'But tell me, tell me! speak again,
Thy soft response renewing—
What makes that ship drive on so fast?
What is the ocean doing?'
Second Voice
Still as a slave before his lord,
The ocean hath no blast;
His great bright eye most silently
Up to the Moon is cast—
If he may know which way to go;
For she guides him smooth or grim.
See, brother, see! how graciously
She looketh down on him.'
First Voice
'But why drives on that ship so fast,
Without or wave or wind?'
Second Voice
'The air is cut away before,
And closes from behind.
Fly, brother, fly! more high, more high!
Or we shall be belated:
For slow and slow that ship will go,
When the Mariner's trance is abated.'
I woke, and we were sailing on
As in a gentle weather:
'Twas night, calm night, the moon was high;
The dead men stood together.
All stood together on the deck,
For a charnel-dungeon fitter:
All fixed on me their stony eyes,
That in the Moon did glitter.
The pang, the curse, with which they died,
Had never passed away:
I could not draw my eyes from theirs,
Nor turn them up to pray.
And now this spell was snapt: once more
I viewed the ocean green,
And looked far forth, yet little saw
Of what had else been seen—
Like one, that on a lonesome road
Doth walk in fear and dread,
And having once turned round walks on,
And turns no more his head;
Because he knows, a frightful fiend
Doth close behind him tread.
But soon there breathed a wind on me,
Nor sound nor motion made:
Its path was not upon the sea,
In ripple or in shade.
It raised my hair, it fanned my cheek
Like a meadow-gale of spring—
It mingled strangely with my fears,
Yet it felt like a welcoming.
Swiftly, swiftly flew the ship,
Yet she sailed softly too:
Sweetly, sweetly blew the breeze—
On me alone it blew.
Oh! dream of joy! is this indeed
The light-house top I see?
Is this the hill? is this the kirk?
Is this mine own countree?
We drifted o'er the harbour-bar,
And I with sobs did pray—
O let me be awake, my God!
Or let me sleep alway.
The harbour-bay was clear as glass,
So smoothly it was strewn!
And on the bay the moonlight lay,
And the shadow of the Moon.
The rock shone bright, the kirk no less,
That stands above the rock:
The moonlight steeped in silentness
The steady weathercock.
And the bay was white with silent light,
Till rising from the same,
Full many shapes, that shadows were,
In crimson colours came.
A little distance from the prow
Those crimson shadows were:
I turned my eyes upon the deck—
Oh, Christ! what saw I there!
Each corse lay flat, lifeless and flat,
And, by the holy rood!
A man all light, a seraph-man,
On every corse there stood.
This seraph-band, each waved his hand:
It was a heavenly sight!
They stood as signals to the land,
Each one a lovely light;
This seraph-band, each waved his hand,
No voice did they impart—
No voice; but oh! the silence sank
Like music on my heart.
But soon I heard the dash of oars,
I heard the Pilot's cheer;
My head was turned perforce away
And I saw a boat appear.
The Pilot and the Pilot's boy,
I heard them coming fast:
Dear Lord in Heaven! it was a joy
The dead men could not blast.
I saw a third—I heard his voice:
It is the Hermit good!
He singeth loud his godly hymns
That he makes in the wood.
He'll shrieve my soul, he'll wash away
The Albatross's blood.
PART VII
This Hermit good lives in that wood
Which slopes down to the sea.
How loudly his sweet voice he rears!
He loves to talk with marineres
That come from a far countree.
He kneels at morn, and noon, and eve—
He hath a cushion plump:
It is the moss that wholly hides
The rotted old oak-stump.
The skiff-boat neared: I heard them talk,
'Why, this is strange, I trow!
Where are those lights so many and fair,
That signal made but now?'
'Strange, by my faith!' the Hermit said—
'And they answered not our cheer!
The planks looked warped! and see those sails,
How thin they are and sere!
I never saw aught like to them,
Unless perchance it were
Brown skeletons of leaves that lag
My forest-brook along;
When the ivy-tod is heavy with snow,
And the owlet whoops to the wolf below,
That eats the she-wolf's young.'
'Dear Lord! it hath a fiendish look—
(The Pilot made reply)
I am a-feared'—'Push on, push on!'
Said the Hermit cheerily.
The boat came closer to the ship,
But I nor spake nor stirred;
The boat came close beneath the ship,
And straight a sound was heard.
Under the water it rumbled on,
Still louder and more dread:
It reached the ship, it split the bay;
The ship went down like lead.
Stunned by that loud and dreadful sound,
Which sky and ocean smote,
Like one that hath been seven days drowned
My body lay afloat;
But swift as dreams, myself I found
Within the Pilot's boat.
Upon the whirl, where sank the ship,
The boat spun round and round;
And all was still, save that the hill
Was telling of the sound.
I moved my lips—the Pilot shrieked
And fell down in a fit;
The holy Hermit raised his eyes,
And prayed where he did sit.
I took the oars: the Pilot's boy,
Who now doth crazy go,
Laughed loud and long, and all the while
His eyes went to and fro.
'Ha! ha!' quoth he, 'full plain I see,
The Devil knows how to row.'
And now, all in my own countree,
I stood on the firm land!
The Hermit stepped forth from the boat,
And scarcely he could stand.
'O shrieve me, shrieve me, holy man!'
The Hermit crossed his brow.
'Say quick,' quoth he, 'I bid thee say—
What manner of man art thou?'
Forthwith this frame of mine was wrenched
With a woful agony,
Which forced me to begin my tale;
And then it left me free.
Since then, at an uncertain hour,
That agony returns:
And till my ghastly tale is told,
This heart within me burns.
I pass, like night, from land to land;
I have strange power of speech;
That moment that his face I see,
I know the man that must hear me:
To him my tale I teach.
What loud uproar bursts from that door!
The wedding-guests are there:
But in the garden-bower the bride
And bride-maids singing are:
And hark the little vesper bell,
Which biddeth me to prayer!
O Wedding-Guest! this soul hath been
Alone on a wide wide sea:
So lonely 'twas, that God himself
Scarce seemèd there to be.
O sweeter than the marriage-feast,
'Tis sweeter far to me,
To walk together to the kirk
With a goodly company!—
To walk together to the kirk,
And all together pray,
While each to his great Father bends,
Old men, and babes, and loving friends
And youths and maidens gay!
Farewell, farewell! but this I tell
To thee, thou Wedding-Guest!
He prayeth well, who loveth well
Both man and bird and beast.
He prayeth best, who loveth best
All things both great and small;
For the dear God who loveth us,
He made and loveth all.
The Mariner, whose eye is bright,
Whose beard with age is hoar,
Is gone: and now the Wedding-Guest
Turned from the bridegroom's door.
He went like one that hath been stunned,
And is of sense forlorn:
A sadder and a wiser man,
He rose the morrow morn.
Loading…
Cancel
Save