net/textproto: new package, with example net/dict

Generic text-based network protcol library for SMTP-like protocols.
HTTP and NNTP should be changed to use this package,
and I expect that SMTP and POP3 will be able to use it too.

R=cemeyer, nigeltao_golang, r
CC=golang-dev, petar-m
https://golang.org/cl/889041
This commit is contained in:
Russ Cox 2010-08-06 17:37:45 -07:00
parent 18063d4686
commit 844a86317d
10 changed files with 1214 additions and 0 deletions

View File

@ -95,6 +95,8 @@ DIRS=\
mime\
mime/multipart\
net\
net/dict\
net/textproto\
netchan\
nntp\
once\
@ -139,6 +141,7 @@ NOTEST=\
http/pprof\
image\
image/jpeg\
net/dict\
rand\
runtime\
runtime/pprof\

View File

@ -0,0 +1,7 @@
include ../../../Make.$(GOARCH)
TARG=net/dict
GOFILES=\
dict.go\
include ../../../Make.pkg

205
src/pkg/net/dict/dict.go Normal file
View File

@ -0,0 +1,205 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package dict implements the Dictionary Server Protocol
// as defined in RFC 2229.
package dict
import (
"container/vector"
"net/textproto"
"os"
"strconv"
"strings"
)
// A Client represents a client connection to a dictionary server.
type Client struct {
text *textproto.Conn
}
// Dial returns a new client connected to a dictionary server at
// addr on the given network.
func Dial(network, addr string) (*Client, os.Error) {
text, err := textproto.Dial(network, addr)
if err != nil {
return nil, err
}
_, _, err = text.ReadCodeLine(220)
if err != nil {
text.Close()
return nil, err
}
return &Client{text: text}, nil
}
// Close closes the connection to the dictionary server.
func (c *Client) Close() os.Error {
return c.text.Close()
}
// A Dict represents a dictionary available on the server.
type Dict struct {
Name string // short name of dictionary
Desc string // long description
}
// Dicts returns a list of the dictionaries available on the server.
func (c *Client) Dicts() ([]Dict, os.Error) {
id, err := c.text.Cmd("SHOW DB")
if err != nil {
return nil, err
}
c.text.StartResponse(id)
defer c.text.EndResponse(id)
lines, err := c.text.ReadDotLines()
if err != nil {
return nil, err
}
_, _, err = c.text.ReadCodeLine(250)
dicts := make([]Dict, len(lines))
for i := range dicts {
d := &dicts[i]
a, _ := fields(lines[i])
if len(a) < 2 {
return nil, textproto.ProtocolError("invalid dictionary: " + lines[i])
}
d.Name = a[0]
d.Desc = a[1]
}
return dicts, err
}
// A Defn represents a definition.
type Defn struct {
Dict Dict // Dict where definition was found
Word string // Word being defined
Text []byte // Definition text, typically multiple lines
}
// Define requests the definition of the given word.
// The argument dict names the dictionary to use,
// the Name field of a Dict returned by Dicts.
//
// The special dictionary name "!" means to look in all the
// server's dictionaries.
// The special dictionary name "*" means to look in all the
// server's dictionaries in turn, stopping after finding the word
// in one of them.
func (c *Client) Define(dict, word string) ([]*Defn, os.Error) {
id, err := c.text.Cmd("DEFINE %s %q", dict, word)
if err != nil {
return nil, err
}
c.text.StartResponse(id)
defer c.text.EndResponse(id)
_, line, err := c.text.ReadCodeLine(150)
a, _ := fields(line)
if len(a) < 1 {
return nil, textproto.ProtocolError("malformed response: " + line)
}
n, err := strconv.Atoi(a[0])
if err != nil {
return nil, textproto.ProtocolError("invalid definition count: " + a[0])
}
def := make([]*Defn, n)
for i := 0; i < n; i++ {
_, line, err = c.text.ReadCodeLine(151)
if err != nil {
return nil, err
}
a, _ := fields(line)
if len(a) < 3 {
// skip it, to keep protocol in sync
i--
n--
def = def[0:n]
continue
}
d := &Defn{Word: a[0], Dict: Dict{a[1], a[2]}}
d.Text, err = c.text.ReadDotBytes()
if err != nil {
return nil, err
}
def[i] = d
}
_, _, err = c.text.ReadCodeLine(250)
return def, err
}
// Fields returns the fields in s.
// Fields are space separated unquoted words
// or quoted with single or double quote.
func fields(s string) ([]string, os.Error) {
var v vector.StringVector
i := 0
for {
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
i++
}
if i >= len(s) {
break
}
if s[i] == '"' || s[i] == '\'' {
q := s[i]
// quoted string
var j int
for j = i + 1; ; j++ {
if j >= len(s) {
return nil, textproto.ProtocolError("malformed quoted string")
}
if s[j] == '\\' {
j++
continue
}
if s[j] == q {
j++
break
}
}
v.Push(unquote(s[i+1 : j-1]))
i = j
} else {
// atom
var j int
for j = i; j < len(s); j++ {
if s[j] == ' ' || s[j] == '\t' || s[j] == '\\' || s[j] == '"' || s[j] == '\'' {
break
}
}
v.Push(s[i:j])
i = j
}
if i < len(s) {
c := s[i]
if c != ' ' && c != '\t' {
return nil, textproto.ProtocolError("quotes not on word boundaries")
}
}
}
return v, nil
}
func unquote(s string) string {
if strings.Index(s, "\\") < 0 {
return s
}
b := []byte(s)
w := 0
for r := 0; r < len(b); r++ {
c := b[r]
if c == '\\' {
r++
c = b[r]
}
b[w] = c
w++
}
return string(b[0:w])
}

View File

@ -0,0 +1,14 @@
# Copyright 2010 The Go Authors. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
include ../../../Make.$(GOARCH)
TARG=net/textproto
GOFILES=\
pipeline.go\
reader.go\
textproto.go\
writer.go\
include ../../../Make.pkg

View File

@ -0,0 +1,117 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package textproto
import (
"sync"
)
// A Pipeline manages a pipelined in-order request/response sequence.
//
// To use a Pipeline p to manage multiple clients on a connection,
// each client should run:
//
// id := p.Next() // take a number
//
// p.StartRequest(id) // wait for turn to send request
// «send request»
// p.EndRequest(id) // notify Pipeline that request is sent
//
// p.StartResponse(id) // wait for turn to read response
// «read response»
// p.EndResponse(id) // notify Pipeline that response is read
//
// A pipelined server can use the same calls to ensure that
// responses computed in parallel are written in the correct order.
type Pipeline struct {
mu sync.Mutex
id uint
request sequencer
response sequencer
}
// Next returns the next id for a request/response pair.
func (p *Pipeline) Next() uint {
p.mu.Lock()
id := p.id
p.id++
p.mu.Unlock()
return id
}
// StartRequest blocks until it is time to send (or, if this is a server, receive)
// the request with the given id.
func (p *Pipeline) StartRequest(id uint) {
p.request.Start(id)
}
// EndRequest notifies p that the request with the given id has been sent
// (or, if this is a server, received).
func (p *Pipeline) EndRequest(id uint) {
p.request.End(id)
}
// StartResponse blocks until it is time to receive (or, if this is a server, send)
// the request with the given id.
func (p *Pipeline) StartResponse(id uint) {
p.response.Start(id)
}
// EndResponse notifies p that the response with the given id has been received
// (or, if this is a server, sent).
func (p *Pipeline) EndResponse(id uint) {
p.response.End(id)
}
// A sequencer schedules a sequence of numbered events that must
// happen in order, one after the other. The event numbering must start
// at 0 and increment without skipping. The event number wraps around
// safely as long as there are not 2^32 simultaneous events pending.
type sequencer struct {
mu sync.Mutex
id uint
wait map[uint]chan uint
}
// Start waits until it is time for the event numbered id to begin.
// That is, except for the first event, it waits until End(id-1) has
// been called.
func (s *sequencer) Start(id uint) {
s.mu.Lock()
if s.id == id {
s.mu.Unlock()
return
}
c := make(chan uint)
if s.wait == nil {
s.wait = make(map[uint]chan uint)
}
s.wait[id] = c
s.mu.Unlock()
<-c
}
// End notifies the sequencer that the event numbered id has completed,
// allowing it to schedule the event numbered id+1. It is a run-time error
// to call End with an id that is not the number of the active event.
func (s *sequencer) End(id uint) {
s.mu.Lock()
if s.id != id {
panic("out of sync")
}
id++
s.id = id
if s.wait == nil {
s.wait = make(map[uint]chan uint)
}
c, ok := s.wait[id]
if ok {
s.wait[id] = nil, false
}
s.mu.Unlock()
if ok {
c <- 1
}
}

View File

@ -0,0 +1,452 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package textproto
import (
"bufio"
"bytes"
"container/vector"
"io"
"io/ioutil"
"os"
"strconv"
)
// BUG(rsc): To let callers manage exposure to denial of service
// attacks, Reader should allow them to set and reset a limit on
// the number of bytes read from the connection.
// A Reader implements convenience methods for reading requests
// or responses from a text protocol network connection.
type Reader struct {
R *bufio.Reader
dot *dotReader
}
// NewReader returns a new Reader reading from r.
func NewReader(r *bufio.Reader) *Reader {
return &Reader{R: r}
}
// ReadLine reads a single line from r,
// eliding the final \n or \r\n from the returned string.
func (r *Reader) ReadLine() (string, os.Error) {
line, err := r.ReadLineBytes()
return string(line), err
}
// ReadLineBytes is like ReadLine but returns a []byte instead of a string.
func (r *Reader) ReadLineBytes() ([]byte, os.Error) {
r.closeDot()
line, err := r.R.ReadBytes('\n')
n := len(line)
if n > 0 && line[n-1] == '\n' {
n--
if n > 0 && line[n-1] == '\r' {
n--
}
}
return line[0:n], err
}
var space = []byte{' '}
// ReadContinuedLine reads a possibly continued line from r,
// eliding the final trailing ASCII white space.
// Lines after the first are considered continuations if they
// begin with a space or tab character. In the returned data,
// continuation lines are separated from the previous line
// only by a single space: the newline and leading white space
// are removed.
//
// For example, consider this input:
//
// Line 1
// continued...
// Line 2
//
// The first call to ReadContinuedLine will return "Line 1 continued..."
// and the second will return "Line 2".
//
// A line consisting of only white space is never continued.
//
func (r *Reader) ReadContinuedLine() (string, os.Error) {
line, err := r.ReadContinuedLineBytes()
return string(line), err
}
// trim returns s with leading and trailing spaces and tabs removed.
// It does not assume Unicode or UTF-8.
func trim(s []byte) []byte {
i := 0
for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
i++
}
n := len(s)
for n > i && (s[n-1] == ' ' || s[n-1] == '\t') {
n--
}
return s[i:n]
}
// ReadContinuedLineBytes is like ReadContinuedLine but
// returns a []byte instead of a string.
func (r *Reader) ReadContinuedLineBytes() ([]byte, os.Error) {
// Read the first line.
line, err := r.ReadLineBytes()
if err != nil {
return line, err
}
if len(line) == 0 { // blank line - no continuation
return line, nil
}
line = trim(line)
// Look for a continuation line.
c, err := r.R.ReadByte()
if err != nil {
// Delay err until we read the byte next time.
return line, nil
}
if c != ' ' && c != '\t' {
// Not a continuation.
r.R.UnreadByte()
return line, nil
}
// Read continuation lines.
for {
// Consume leading spaces; one already gone.
for {
c, err = r.R.ReadByte()
if err != nil {
break
}
if c != ' ' && c != '\t' {
r.R.UnreadByte()
break
}
}
var cont []byte
cont, err = r.ReadLineBytes()
cont = trim(cont)
line = bytes.Add(line, space)
line = bytes.Add(line, cont)
if err != nil {
break
}
// Check for leading space on next line.
if c, err = r.R.ReadByte(); err != nil {
break
}
if c != ' ' && c != '\t' {
r.R.UnreadByte()
break
}
}
// Delay error until next call.
if len(line) > 0 {
err = nil
}
return line, err
}
// ReadCodeLine reads a response code line of the form
// code message
// where code is a 3-digit status code and the message
// extends to the rest of the line. An example of such a line is:
// 220 plan9.bell-labs.com ESMTP
//
// If the prefix of the status does not match the digits in expectCode,
// ReadCodeLine returns with err set to &Error{code, message}.
// For example, if expectCode is 31, an error will be returned if
// the status is not in the range [310,319].
//
// An expectCode <= 0 disables the check of the status code.
//
func (r *Reader) ReadCodeLine(expectCode int) (code int, message string, err os.Error) {
line, err := r.ReadLine()
if err != nil {
return
}
if len(line) < 4 || line[3] != ' ' {
err = ProtocolError("short response: " + line)
return
}
code, err = strconv.Atoi(line[0:3])
if err != nil || code < 100 {
err = ProtocolError("invalid response code: " + line)
return
}
message = line[4:]
if 1 <= expectCode && expectCode < 10 && code/100 != expectCode ||
10 <= expectCode && expectCode < 100 && code/10 != expectCode ||
100 <= expectCode && expectCode < 1000 && code != expectCode {
err = &Error{code, message}
}
return
}
// DotReader returns a new Reader that satisfies Reads using the
// decoded text of a dot-encoded block read from r.
// The returned Reader is only valid until the next call
// to a method on r.
//
// Dot encoding is a common framing used for data blocks
// in text protcols like SMTP. The data consists of a sequence
// of lines, each of which ends in "\r\n". The sequence itself
// ends at a line containing just a dot: ".\r\n". Lines beginning
// with a dot are escaped with an additional dot to avoid
// looking like the end of the sequence.
//
// The decoded form returned by the Reader's Read method
// rewrites the "\r\n" line endings into the simpler "\n",
// removes leading dot escapes if present, and stops with error os.EOF
// after consuming (and discarding) the end-of-sequence line.
func (r *Reader) DotReader() io.Reader {
r.closeDot()
r.dot = &dotReader{r: r}
return r.dot
}
type dotReader struct {
r *Reader
state int
}
// Read satisfies reads by decoding dot-encoded data read from d.r.
func (d *dotReader) Read(b []byte) (n int, err os.Error) {
// Run data through a simple state machine to
// elide leading dots, rewrite trailing \r\n into \n,
// and detect ending .\r\n line.
const (
stateBeginLine = iota // beginning of line; initial state; must be zero
stateDot // read . at beginning of line
stateDotCR // read .\r at beginning of line
stateCR // read \r (possibly at end of line)
stateData // reading data in middle of line
stateEOF // reached .\r\n end marker line
)
br := d.r.R
for n < len(b) && d.state != stateEOF {
var c byte
c, err = br.ReadByte()
if err != nil {
if err == os.EOF {
err = io.ErrUnexpectedEOF
}
break
}
switch d.state {
case stateBeginLine:
if c == '.' {
d.state = stateDot
continue
}
if c == '\r' {
d.state = stateCR
continue
}
d.state = stateData
case stateDot:
if c == '\r' {
d.state = stateDotCR
continue
}
if c == '\n' {
d.state = stateEOF
continue
}
d.state = stateData
case stateDotCR:
if c == '\n' {
d.state = stateEOF
continue
}
// Not part of .\r\n.
// Consume leading dot and emit saved \r.
br.UnreadByte()
c = '\r'
d.state = stateData
case stateCR:
if c == '\n' {
d.state = stateBeginLine
break
}
// Not part of \r\n. Emit saved \r
br.UnreadByte()
c = '\r'
d.state = stateData
case stateData:
if c == '\r' {
d.state = stateCR
continue
}
if c == '\n' {
d.state = stateBeginLine
}
}
b[n] = c
n++
}
if err == nil && d.state == stateEOF {
err = os.EOF
}
if err != nil && d.r.dot == d {
d.r.dot = nil
}
return
}
// closeDot drains the current DotReader if any,
// making sure that it reads until the ending dot line.
func (r *Reader) closeDot() {
if r.dot == nil {
return
}
buf := make([]byte, 128)
for r.dot != nil {
// When Read reaches EOF or an error,
// it will set r.dot == nil.
r.dot.Read(buf)
}
}
// ReadDotBytes reads a dot-encoding and returns the decoded data.
//
// See the documentation for the DotReader method for details about dot-encoding.
func (r *Reader) ReadDotBytes() ([]byte, os.Error) {
return ioutil.ReadAll(r.DotReader())
}
// ReadDotLines reads a dot-encoding and returns a slice
// containing the decoded lines, with the final \r\n or \n elided from each.
//
// See the documentation for the DotReader method for details about dot-encoding.
func (r *Reader) ReadDotLines() ([]string, os.Error) {
// We could use ReadDotBytes and then Split it,
// but reading a line at a time avoids needing a
// large contiguous block of memory and is simpler.
var v vector.StringVector
var err os.Error
for {
var line string
line, err = r.ReadLine()
if err != nil {
if err == os.EOF {
err = io.ErrUnexpectedEOF
}
break
}
// Dot by itself marks end; otherwise cut one dot.
if len(line) > 0 && line[0] == '.' {
if len(line) == 1 {
break
}
line = line[1:]
}
v.Push(line)
}
return v, err
}
// ReadMIMEHeader reads a MIME-style header from r.
// The header is a sequence of possibly continued Key: Value lines
// ending in a blank line.
// The returned map m maps CanonicalHeaderKey(key) to a
// sequence of values in the same order encountered in the input.
//
// For example, consider this input:
//
// My-Key: Value 1
// Long-Key: Even
// Longer Value
// My-Key: Value 2
//
// Given that input, ReadMIMEHeader returns the map:
//
// map[string][]string{
// "My-Key": []string{"Value 1", "Value 2"},
// "Long-Key": []string{"Even Longer Value"},
// }
//
func (r *Reader) ReadMIMEHeader() (map[string][]string, os.Error) {
m := make(map[string][]string)
for {
kv, err := r.ReadContinuedLineBytes()
if len(kv) == 0 {
return m, err
}
// Key ends at first colon; must not have spaces.
i := bytes.IndexByte(kv, ':')
if i < 0 || bytes.IndexByte(kv[0:i], ' ') >= 0 {
return m, ProtocolError("malformed MIME header line: " + string(kv))
}
key := CanonicalHeaderKey(string(kv[0:i]))
// Skip initial spaces in value.
i++ // skip colon
for i < len(kv) && (kv[i] == ' ' || kv[i] == '\t') {
i++
}
value := string(kv[i:])
v := vector.StringVector(m[key])
v.Push(value)
m[key] = v
if err != nil {
return m, err
}
}
panic("unreachable")
}
// CanonicalHeaderKey returns the canonical format of the
// MIME header key s. The canonicalization converts the first
// letter and any letter following a hyphen to upper case;
// the rest are converted to lowercase. For example, the
// canonical key for "accept-encoding" is "Accept-Encoding".
func CanonicalHeaderKey(s string) string {
// Quick check for canonical encoding.
needUpper := true
for i := 0; i < len(s); i++ {
c := s[i]
if needUpper && 'a' <= c && c <= 'z' {
goto MustRewrite
}
if !needUpper && 'A' <= c && c <= 'Z' {
goto MustRewrite
}
needUpper = c == '-'
}
return s
MustRewrite:
// Canonicalize: first letter upper case
// and upper case after each dash.
// (Host, User-Agent, If-Modified-Since).
// MIME headers are ASCII only, so no Unicode issues.
a := []byte(s)
upper := true
for i, v := range a {
if upper && 'a' <= v && v <= 'z' {
a[i] = v + 'A' - 'a'
}
if !upper && 'A' <= v && v <= 'Z' {
a[i] = v + 'a' - 'A'
}
upper = v == '-'
}
return string(a)
}

View File

@ -0,0 +1,140 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package textproto
import (
"bufio"
"io"
"os"
"reflect"
"strings"
"testing"
)
type canonicalHeaderKeyTest struct {
in, out string
}
var canonicalHeaderKeyTests = []canonicalHeaderKeyTest{
canonicalHeaderKeyTest{"a-b-c", "A-B-C"},
canonicalHeaderKeyTest{"a-1-c", "A-1-C"},
canonicalHeaderKeyTest{"User-Agent", "User-Agent"},
canonicalHeaderKeyTest{"uSER-aGENT", "User-Agent"},
canonicalHeaderKeyTest{"user-agent", "User-Agent"},
canonicalHeaderKeyTest{"USER-AGENT", "User-Agent"},
}
func TestCanonicalHeaderKey(t *testing.T) {
for _, tt := range canonicalHeaderKeyTests {
if s := CanonicalHeaderKey(tt.in); s != tt.out {
t.Errorf("CanonicalHeaderKey(%q) = %q, want %q", tt.in, s, tt.out)
}
}
}
func reader(s string) *Reader {
return NewReader(bufio.NewReader(strings.NewReader(s)))
}
func TestReadLine(t *testing.T) {
r := reader("line1\nline2\n")
s, err := r.ReadLine()
if s != "line1" || err != nil {
t.Fatalf("Line 1: %s, %v", s, err)
}
s, err = r.ReadLine()
if s != "line2" || err != nil {
t.Fatalf("Line 2: %s, %v", s, err)
}
s, err = r.ReadLine()
if s != "" || err != os.EOF {
t.Fatalf("EOF: %s, %v", s, err)
}
}
func TestReadContinuedLine(t *testing.T) {
r := reader("line1\nline\n 2\nline3\n")
s, err := r.ReadContinuedLine()
if s != "line1" || err != nil {
t.Fatalf("Line 1: %s, %v", s, err)
}
s, err = r.ReadContinuedLine()
if s != "line 2" || err != nil {
t.Fatalf("Line 2: %s, %v", s, err)
}
s, err = r.ReadContinuedLine()
if s != "line3" || err != nil {
t.Fatalf("Line 3: %s, %v", s, err)
}
s, err = r.ReadContinuedLine()
if s != "" || err != os.EOF {
t.Fatalf("EOF: %s, %v", s, err)
}
}
func TestReadCodeLine(t *testing.T) {
r := reader("123 hi\n234 bye\n345 no way\n")
code, msg, err := r.ReadCodeLine(0)
if code != 123 || msg != "hi" || err != nil {
t.Fatalf("Line 1: %d, %s, %v", code, msg, err)
}
code, msg, err = r.ReadCodeLine(23)
if code != 234 || msg != "bye" || err != nil {
t.Fatalf("Line 2: %d, %s, %v", code, msg, err)
}
code, msg, err = r.ReadCodeLine(346)
if code != 345 || msg != "no way" || err == nil {
t.Fatalf("Line 3: %d, %s, %v", code, msg, err)
}
if e, ok := err.(*Error); !ok || e.Code != code || e.Msg != msg {
t.Fatalf("Line 3: wrong error %v\n", err)
}
code, msg, err = r.ReadCodeLine(1)
if code != 0 || msg != "" || err != os.EOF {
t.Fatalf("EOF: %d, %s, %v", code, msg, err)
}
}
func TestReadDotLines(t *testing.T) {
r := reader("dotlines\r\n.foo\r\n..bar\n...baz\nquux\r\n\r\n.\r\nanother\n")
s, err := r.ReadDotLines()
want := []string{"dotlines", "foo", ".bar", "..baz", "quux", ""}
if !reflect.DeepEqual(s, want) || err != nil {
t.Fatalf("ReadDotLines: %v, %v", s, err)
}
s, err = r.ReadDotLines()
want = []string{"another"}
if !reflect.DeepEqual(s, want) || err != io.ErrUnexpectedEOF {
t.Fatalf("ReadDotLines2: %v, %v", s, err)
}
}
func TestReadDotBytes(t *testing.T) {
r := reader("dotlines\r\n.foo\r\n..bar\n...baz\nquux\r\n\r\n.\r\nanot.her\r\n")
b, err := r.ReadDotBytes()
want := []byte("dotlines\nfoo\n.bar\n..baz\nquux\n\n")
if !reflect.DeepEqual(b, want) || err != nil {
t.Fatalf("ReadDotBytes: %q, %v", b, err)
}
b, err = r.ReadDotBytes()
want = []byte("anot.her\n")
if !reflect.DeepEqual(b, want) || err != io.ErrUnexpectedEOF {
t.Fatalf("ReadDotBytes2: %q, %v", b, err)
}
}
func TestReadMIMEHeader(t *testing.T) {
r := reader("my-key: Value 1 \r\nLong-key: Even \n Longer Value\r\nmy-Key: Value 2\r\n\n")
m, err := r.ReadMIMEHeader()
want := map[string][]string{
"My-Key": []string{"Value 1", "Value 2"},
"Long-Key": []string{"Even Longer Value"},
}
if !reflect.DeepEqual(m, want) || err != nil {
t.Fatalf("ReadMIMEHeader: %v, %v; want %v", m, err, want)
}
}

View File

@ -0,0 +1,122 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The textproto package implements generic support for
// text-based request/response protocols in the style of
// HTTP, NNTP, and SMTP.
//
// The package provides:
//
// Error, which represents a numeric error response from
// a server.
//
// Pipeline, to manage pipelined requests and responses
// in a client.
//
// Reader, to read numeric response code lines,
// key: value headers, lines wrapped with leading spaces
// on continuation lines, and whole text blocks ending
// with a dot on a line by itself.
//
// Writer, to write dot-encoded text blocks.
//
package textproto
import (
"bufio"
"fmt"
"io"
"net"
"os"
)
// An Error represents a numeric error response from a server.
type Error struct {
Code int
Msg string
}
func (e *Error) String() string {
return fmt.Sprintf("%03d %s", e.Code, e.Msg)
}
// A ProtocolError describes a protocol violation such
// as an invalid response or a hung-up connection.
type ProtocolError string
func (p ProtocolError) String() string {
return string(p)
}
// A Conn represents a textual network protocol connection.
// It consists of a Reader and Writer to manage I/O
// and a Pipeline to sequence concurrent requests on the connection.
// These embedded types carry methods with them;
// see the documentation of those types for details.
type Conn struct {
Reader
Writer
Pipeline
conn io.ReadWriteCloser
}
// NewConn returns a new Conn using conn for I/O.
func NewConn(conn io.ReadWriteCloser) *Conn {
return &Conn{
Reader: Reader{R: bufio.NewReader(conn)},
Writer: Writer{W: bufio.NewWriter(conn)},
conn: conn,
}
}
// Close closes the connection.
func (c *Conn) Close() os.Error {
return c.conn.Close()
}
// Dial connects to the given address on the given network using net.Dial
// and then returns a new Conn for the connection.
func Dial(network, addr string) (*Conn, os.Error) {
c, err := net.Dial(network, "", addr)
if err != nil {
return nil, err
}
return NewConn(c), nil
}
// Cmd is a convenience method that sends a command after
// waiting its turn in the pipeline. The command text is the
// result of formatting format with args and appending \r\n.
// Cmd returns the id of the command, for use with StartResponse and EndResponse.
//
// For example, a client might run a HELP command that returns a dot-body
// by using:
//
// id, err := c.Cmd("HELP")
// if err != nil {
// return nil, err
// }
//
// c.StartResponse(id)
// defer c.EndResponse(id)
//
// if _, _, err = c.ReadCodeLine(110); err != nil {
// return nil, err
// }
// text, err := c.ReadDotAll()
// if err != nil {
// return nil, err
// }
// return c.ReadCodeLine(250)
//
func (c *Conn) Cmd(format string, args ...interface{}) (id uint, err os.Error) {
id = c.Next()
c.StartRequest(id)
err = c.PrintfLine(format, args)
c.EndRequest(id)
if err != nil {
return 0, err
}
return id, nil
}

View File

@ -0,0 +1,119 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package textproto
import (
"bufio"
"fmt"
"io"
"os"
)
// A Writer implements convenience methods for writing
// requests or responses to a text protocol network connection.
type Writer struct {
W *bufio.Writer
dot *dotWriter
}
// NewWriter returns a new Writer writing to w.
func NewWriter(w *bufio.Writer) *Writer {
return &Writer{W: w}
}
var crnl = []byte{'\r', '\n'}
var dotcrnl = []byte{'.', '\r', '\n'}
// PrintfLine writes the formatted output followed by \r\n.
func (w *Writer) PrintfLine(format string, args ...interface{}) os.Error {
w.closeDot()
fmt.Fprintf(w.W, format, args)
w.W.Write(crnl)
return w.W.Flush()
}
// DotWriter returns a writer that can be used to write a dot-encoding to w.
// It takes care of inserting leading dots when necessary,
// translating line-ending \n into \r\n, and adding the final .\r\n line
// when the DotWriter is closed. The caller should close the
// DotWriter before the next call to a method on w.
//
// See the documentation for Reader's DotReader method for details about dot-encoding.
func (w *Writer) DotWriter() io.WriteCloser {
w.closeDot()
w.dot = &dotWriter{w: w}
return w.dot
}
func (w *Writer) closeDot() {
if w.dot != nil {
w.dot.Close() // sets w.dot = nil
}
}
type dotWriter struct {
w *Writer
state int
}
const (
wstateBeginLine = iota // beginning of line; initial state; must be zero
wstateCR // wrote \r (possibly at end of line)
wstateData // writing data in middle of line
)
func (d *dotWriter) Write(b []byte) (n int, err os.Error) {
bw := d.w.W
for n < len(b) {
c := b[n]
switch d.state {
case wstateBeginLine:
d.state = wstateData
if c == '.' {
// escape leading dot
bw.WriteByte('.')
}
fallthrough
case wstateData:
if c == '\r' {
d.state = wstateCR
}
if c == '\n' {
bw.WriteByte('\r')
d.state = wstateBeginLine
}
case wstateCR:
d.state = wstateData
if c == '\n' {
d.state = wstateBeginLine
}
}
if err = bw.WriteByte(c); err != nil {
break
}
n++
}
return
}
func (d *dotWriter) Close() os.Error {
if d.w.dot == d {
d.w.dot = nil
}
bw := d.w.W
switch d.state {
default:
bw.WriteByte('\r')
fallthrough
case wstateCR:
bw.WriteByte('\n')
fallthrough
case wstateBeginLine:
bw.Write(dotcrnl)
}
return bw.Flush()
}

View File

@ -0,0 +1,35 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package textproto
import (
"bufio"
"bytes"
"testing"
)
func TestPrintfLine(t *testing.T) {
var buf bytes.Buffer
w := NewWriter(bufio.NewWriter(&buf))
err := w.PrintfLine("foo %d", 123)
if s := buf.String(); s != "foo 123\r\n" || err != nil {
t.Fatalf("s=%q; err=%s", s, err)
}
}
func TestDotWriter(t *testing.T) {
var buf bytes.Buffer
w := NewWriter(bufio.NewWriter(&buf))
d := w.DotWriter()
n, err := d.Write([]byte("abc\n.def\n..ghi\n.jkl\n."))
if n != 21 || err != nil {
t.Fatalf("Write: %d, %s", n, err)
}
d.Close()
want := "abc\r\n..def\r\n...ghi\r\n..jkl\r\n..\r\n.\r\n"
if s := buf.String(); s != want {
t.Fatalf("wrote %q", s)
}
}