diff options
Diffstat (limited to 'libgo/go/mime')
-rw-r--r-- | libgo/go/mime/grammar.go | 36 | ||||
-rw-r--r-- | libgo/go/mime/mediatype.go | 120 | ||||
-rw-r--r-- | libgo/go/mime/mediatype_test.go | 117 | ||||
-rw-r--r-- | libgo/go/mime/mime_test.go | 27 | ||||
-rw-r--r-- | libgo/go/mime/multipart/multipart.go | 280 | ||||
-rw-r--r-- | libgo/go/mime/multipart/multipart_test.go | 204 | ||||
-rw-r--r-- | libgo/go/mime/test.types | 8 | ||||
-rw-r--r-- | libgo/go/mime/type.go | 104 |
8 files changed, 896 insertions, 0 deletions
diff --git a/libgo/go/mime/grammar.go b/libgo/go/mime/grammar.go new file mode 100644 index 000000000..e60cbb8df --- /dev/null +++ b/libgo/go/mime/grammar.go @@ -0,0 +1,36 @@ +// 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 mime + +import ( + "strings" +) + +// isTSpecial returns true if rune is in 'tspecials' as defined by RFC +// 1531 and RFC 2045. +func isTSpecial(rune int) bool { + return strings.IndexRune(`()<>@,;:\"/[]?=`, rune) != -1 +} + +// IsTokenChar returns true if rune is in 'token' as defined by RFC +// 1531 and RFC 2045. +func IsTokenChar(rune int) bool { + // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, + // or tspecials> + return rune > 0x20 && rune < 0x7f && !isTSpecial(rune) +} + +// IsQText returns true if rune is in 'qtext' as defined by RFC 822. +func IsQText(rune int) bool { + // CHAR = <any ASCII character> ; ( 0-177, 0.-127.) + // qtext = <any CHAR excepting <">, ; => may be folded + // "\" & CR, and including + // linear-white-space> + switch rune { + case '"', '\\', '\r': + return false + } + return rune < 0x80 +} diff --git a/libgo/go/mime/mediatype.go b/libgo/go/mime/mediatype.go new file mode 100644 index 000000000..eb629aa6f --- /dev/null +++ b/libgo/go/mime/mediatype.go @@ -0,0 +1,120 @@ +// 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 mime + +import ( + "bytes" + "strings" + "unicode" +) + +// ParseMediaType parses a media type value and any optional +// parameters, per RFC 1531. Media types are the values in +// Content-Type and Content-Disposition headers (RFC 2183). On +// success, ParseMediaType returns the media type converted to +// lowercase and trimmed of white space and a non-nil params. On +// error, it returns an empty string and a nil params. +func ParseMediaType(v string) (mediatype string, params map[string]string) { + i := strings.Index(v, ";") + if i == -1 { + i = len(v) + } + mediatype = strings.TrimSpace(strings.ToLower(v[0:i])) + params = make(map[string]string) + + v = v[i:] + for len(v) > 0 { + v = strings.TrimLeftFunc(v, unicode.IsSpace) + if len(v) == 0 { + return + } + key, value, rest := consumeMediaParam(v) + if key == "" { + // Parse error. + return "", nil + } + params[key] = value + v = rest + } + return +} + +func isNotTokenChar(rune int) bool { + return !IsTokenChar(rune) +} + +// consumeToken consumes a token from the beginning of provided +// string, per RFC 2045 section 5.1 (referenced from 2183), and return +// the token consumed and the rest of the string. Returns ("", v) on +// failure to consume at least one character. +func consumeToken(v string) (token, rest string) { + notPos := strings.IndexFunc(v, isNotTokenChar) + if notPos == -1 { + return v, "" + } + if notPos == 0 { + return "", v + } + return v[0:notPos], v[notPos:] +} + +// consumeValue consumes a "value" per RFC 2045, where a value is +// either a 'token' or a 'quoted-string'. On success, consumeValue +// returns the value consumed (and de-quoted/escaped, if a +// quoted-string) and the rest of the string. On failure, returns +// ("", v). +func consumeValue(v string) (value, rest string) { + if !strings.HasPrefix(v, `"`) { + return consumeToken(v) + } + + // parse a quoted-string + rest = v[1:] // consume the leading quote + buffer := new(bytes.Buffer) + var idx, rune int + var nextIsLiteral bool + for idx, rune = range rest { + switch { + case nextIsLiteral: + if rune >= 0x80 { + return "", v + } + buffer.WriteRune(rune) + nextIsLiteral = false + case rune == '"': + return buffer.String(), rest[idx+1:] + case IsQText(rune): + buffer.WriteRune(rune) + case rune == '\\': + nextIsLiteral = true + default: + return "", v + } + } + return "", v +} + +func consumeMediaParam(v string) (param, value, rest string) { + rest = strings.TrimLeftFunc(v, unicode.IsSpace) + if !strings.HasPrefix(rest, ";") { + return "", "", v + } + + rest = rest[1:] // consume semicolon + rest = strings.TrimLeftFunc(rest, unicode.IsSpace) + param, rest = consumeToken(rest) + if param == "" { + return "", "", v + } + if !strings.HasPrefix(rest, "=") { + return "", "", v + } + rest = rest[1:] // consume equals sign + value, rest = consumeValue(rest) + if value == "" { + return "", "", v + } + return param, value, rest +} diff --git a/libgo/go/mime/mediatype_test.go b/libgo/go/mime/mediatype_test.go new file mode 100644 index 000000000..4891e899d --- /dev/null +++ b/libgo/go/mime/mediatype_test.go @@ -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 mime + +import ( + "testing" +) + +func TestConsumeToken(t *testing.T) { + tests := [...][3]string{ + {"foo bar", "foo", " bar"}, + {"bar", "bar", ""}, + {"", "", ""}, + {" foo", "", " foo"}, + } + for _, test := range tests { + token, rest := consumeToken(test[0]) + expectedToken := test[1] + expectedRest := test[2] + if token != expectedToken { + t.Errorf("expected to consume token '%s', not '%s' from '%s'", + expectedToken, token, test[0]) + } else if rest != expectedRest { + t.Errorf("expected to have left '%s', not '%s' after reading token '%s' from '%s'", + expectedRest, rest, token, test[0]) + } + } +} + +func TestConsumeValue(t *testing.T) { + tests := [...][3]string{ + {"foo bar", "foo", " bar"}, + {"bar", "bar", ""}, + {" bar ", "", " bar "}, + {`"My value"end`, "My value", "end"}, + {`"My value" end`, "My value", " end"}, + {`"\\" rest`, "\\", " rest"}, + {`"My \" value"end`, "My \" value", "end"}, + {`"\" rest`, "", `"\" rest`}, + } + for _, test := range tests { + value, rest := consumeValue(test[0]) + expectedValue := test[1] + expectedRest := test[2] + if value != expectedValue { + t.Errorf("expected to consume value [%s], not [%s] from [%s]", + expectedValue, value, test[0]) + } else if rest != expectedRest { + t.Errorf("expected to have left [%s], not [%s] after reading value [%s] from [%s]", + expectedRest, rest, value, test[0]) + } + } +} + +func TestConsumeMediaParam(t *testing.T) { + tests := [...][4]string{ + {" ; foo=bar", "foo", "bar", ""}, + {"; foo=bar", "foo", "bar", ""}, + {";foo=bar", "foo", "bar", ""}, + {`;foo="bar"`, "foo", "bar", ""}, + {`;foo="bar"; `, "foo", "bar", "; "}, + {`;foo="bar"; foo=baz`, "foo", "bar", "; foo=baz"}, + {` ; boundary=----CUT;`, "boundary", "----CUT", ";"}, + {` ; key=value; blah="value";name="foo" `, "key", "value", `; blah="value";name="foo" `}, + {`; blah="value";name="foo" `, "blah", "value", `;name="foo" `}, + {`;name="foo" `, "name", "foo", ` `}, + } + for _, test := range tests { + param, value, rest := consumeMediaParam(test[0]) + expectedParam := test[1] + expectedValue := test[2] + expectedRest := test[3] + if param != expectedParam { + t.Errorf("expected to consume param [%s], not [%s] from [%s]", + expectedParam, param, test[0]) + } else if value != expectedValue { + t.Errorf("expected to consume value [%s], not [%s] from [%s]", + expectedValue, value, test[0]) + } else if rest != expectedRest { + t.Errorf("expected to have left [%s], not [%s] after reading [%s/%s] from [%s]", + expectedRest, rest, param, value, test[0]) + } + } +} + +func TestParseMediaType(t *testing.T) { + tests := [...]string{ + `form-data; name="foo"`, + ` form-data ; name=foo`, + `FORM-DATA;name="foo"`, + ` FORM-DATA ; name="foo"`, + ` FORM-DATA ; name="foo"`, + `form-data; key=value; blah="value";name="foo" `, + } + for _, test := range tests { + mt, params := ParseMediaType(test) + if mt != "form-data" { + t.Errorf("expected type form-data for %s, got [%s]", test, mt) + continue + } + if params["name"] != "foo" { + t.Errorf("expected name=foo for %s", test) + } + } +} + +func TestParseMediaTypeBogus(t *testing.T) { + mt, params := ParseMediaType("bogus ;=========") + if mt != "" { + t.Error("expected empty type") + } + if params != nil { + t.Error("expected nil params") + } +} diff --git a/libgo/go/mime/mime_test.go b/libgo/go/mime/mime_test.go new file mode 100644 index 000000000..17e610443 --- /dev/null +++ b/libgo/go/mime/mime_test.go @@ -0,0 +1,27 @@ +// 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. + +// Tests for type.go + +package mime + +import "testing" + +var typeTests = map[string]string{ + ".t1": "application/test", + ".t2": "text/test; charset=utf-8", + ".png": "image/png", +} + +func TestType(t *testing.T) { + typeFiles = []string{"test.types"} + + for ext, want := range typeTests { + val := TypeByExtension(ext) + if val != want { + t.Errorf("TypeByExtension(%q) = %q, want %q", ext, val, want) + } + + } +} diff --git a/libgo/go/mime/multipart/multipart.go b/libgo/go/mime/multipart/multipart.go new file mode 100644 index 000000000..1d855c74c --- /dev/null +++ b/libgo/go/mime/multipart/multipart.go @@ -0,0 +1,280 @@ +// 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 multipart implements MIME multipart parsing, as defined in RFC +2046. + +The implementation is sufficient for HTTP (RFC 2388) and the multipart +bodies generated by popular browsers. +*/ +package multipart + +import ( + "bufio" + "bytes" + "io" + "mime" + "os" + "regexp" + "strings" +) + +var headerRegexp *regexp.Regexp = regexp.MustCompile("^([a-zA-Z0-9\\-]+): *([^\r\n]+)") + +// Reader is an iterator over parts in a MIME multipart body. +// Reader's underlying parser consumes its input as needed. Seeking +// isn't supported. +type Reader interface { + // NextPart returns the next part in the multipart, or (nil, + // nil) on EOF. An error is returned if the underlying reader + // reports errors, or on truncated or otherwise malformed + // input. + NextPart() (*Part, os.Error) +} + +// A Part represents a single part in a multipart body. +type Part struct { + // The headers of the body, if any, with the keys canonicalized + // in the same fashion that the Go http.Request headers are. + // i.e. "foo-bar" changes case to "Foo-Bar" + Header map[string]string + + buffer *bytes.Buffer + mr *multiReader +} + +// FormName returns the name parameter if p has a Content-Disposition +// of type "form-data". Otherwise it returns the empty string. +func (p *Part) FormName() string { + // See http://tools.ietf.org/html/rfc2183 section 2 for EBNF + // of Content-Disposition value format. + v, ok := p.Header["Content-Disposition"] + if !ok { + return "" + } + d, params := mime.ParseMediaType(v) + if d != "form-data" { + return "" + } + return params["name"] +} + +// NewReader creates a new multipart Reader reading from r using the +// given MIME boundary. +func NewReader(reader io.Reader, boundary string) Reader { + return &multiReader{ + boundary: boundary, + dashBoundary: "--" + boundary, + endLine: "--" + boundary + "--", + bufReader: bufio.NewReader(reader), + } +} + +// Implementation .... + +type devNullWriter bool + +func (*devNullWriter) Write(p []byte) (n int, err os.Error) { + return len(p), nil +} + +var devNull = devNullWriter(false) + +func newPart(mr *multiReader) (bp *Part, err os.Error) { + bp = new(Part) + bp.Header = make(map[string]string) + bp.mr = mr + bp.buffer = new(bytes.Buffer) + if err = bp.populateHeaders(); err != nil { + bp = nil + } + return +} + +func (bp *Part) populateHeaders() os.Error { + for { + line, err := bp.mr.bufReader.ReadString('\n') + if err != nil { + return err + } + if line == "\n" || line == "\r\n" { + return nil + } + if matches := headerRegexp.FindStringSubmatch(line); len(matches) == 3 { + key := matches[1] + value := matches[2] + // TODO: canonicalize headers ala http.Request.Header? + bp.Header[key] = value + continue + } + return os.NewError("Unexpected header line found parsing multipart body") + } + panic("unreachable") +} + +// Read reads the body of a part, after its headers and before the +// next part (if any) begins. +func (bp *Part) Read(p []byte) (n int, err os.Error) { + for { + if bp.buffer.Len() >= len(p) { + // Internal buffer of unconsumed data is large enough for + // the read request. No need to parse more at the moment. + break + } + if !bp.mr.ensureBufferedLine() { + return 0, io.ErrUnexpectedEOF + } + if bp.mr.bufferedLineIsBoundary() { + // Don't consume this line + break + } + + // Write all of this line, except the final CRLF + s := *bp.mr.bufferedLine + if strings.HasSuffix(s, "\r\n") { + bp.mr.consumeLine() + if !bp.mr.ensureBufferedLine() { + return 0, io.ErrUnexpectedEOF + } + if bp.mr.bufferedLineIsBoundary() { + // The final \r\n isn't ours. It logically belongs + // to the boundary line which follows. + bp.buffer.WriteString(s[0 : len(s)-2]) + } else { + bp.buffer.WriteString(s) + } + break + } + if strings.HasSuffix(s, "\n") { + bp.buffer.WriteString(s) + bp.mr.consumeLine() + continue + } + return 0, os.NewError("multipart parse error during Read; unexpected line: " + s) + } + return bp.buffer.Read(p) +} + +func (bp *Part) Close() os.Error { + io.Copy(&devNull, bp) + return nil +} + +type multiReader struct { + boundary string + dashBoundary string // --boundary + endLine string // --boundary-- + + bufferedLine *string + + bufReader *bufio.Reader + currentPart *Part + partsRead int +} + +func (mr *multiReader) eof() bool { + return mr.bufferedLine == nil && + !mr.readLine() +} + +func (mr *multiReader) readLine() bool { + line, err := mr.bufReader.ReadString('\n') + if err != nil { + // TODO: care about err being EOF or not? + return false + } + mr.bufferedLine = &line + return true +} + +func (mr *multiReader) bufferedLineIsBoundary() bool { + return strings.HasPrefix(*mr.bufferedLine, mr.dashBoundary) +} + +func (mr *multiReader) ensureBufferedLine() bool { + if mr.bufferedLine == nil { + return mr.readLine() + } + return true +} + +func (mr *multiReader) consumeLine() { + mr.bufferedLine = nil +} + +func (mr *multiReader) NextPart() (*Part, os.Error) { + if mr.currentPart != nil { + mr.currentPart.Close() + } + + for { + if mr.eof() { + return nil, io.ErrUnexpectedEOF + } + + if isBoundaryDelimiterLine(*mr.bufferedLine, mr.dashBoundary) { + mr.consumeLine() + mr.partsRead++ + bp, err := newPart(mr) + if err != nil { + return nil, err + } + mr.currentPart = bp + return bp, nil + } + + if hasPrefixThenNewline(*mr.bufferedLine, mr.endLine) { + mr.consumeLine() + // Expected EOF (no error) + return nil, nil + } + + if mr.partsRead == 0 { + // skip line + mr.consumeLine() + continue + } + + return nil, os.NewError("Unexpected line in Next().") + } + panic("unreachable") +} + +func isBoundaryDelimiterLine(line, dashPrefix string) bool { + // http://tools.ietf.org/html/rfc2046#section-5.1 + // The boundary delimiter line is then defined as a line + // consisting entirely of two hyphen characters ("-", + // decimal value 45) followed by the boundary parameter + // value from the Content-Type header field, optional linear + // whitespace, and a terminating CRLF. + if !strings.HasPrefix(line, dashPrefix) { + return false + } + if strings.HasSuffix(line, "\r\n") { + return onlyHorizontalWhitespace(line[len(dashPrefix) : len(line)-2]) + } + // Violate the spec and also support newlines without the + // carriage return... + if strings.HasSuffix(line, "\n") { + return onlyHorizontalWhitespace(line[len(dashPrefix) : len(line)-1]) + } + return false +} + +func onlyHorizontalWhitespace(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] != ' ' && s[i] != '\t' { + return false + } + } + return true +} + +func hasPrefixThenNewline(s, prefix string) bool { + return strings.HasPrefix(s, prefix) && + (len(s) == len(prefix)+1 && strings.HasSuffix(s, "\n") || + len(s) == len(prefix)+2 && strings.HasSuffix(s, "\r\n")) +} diff --git a/libgo/go/mime/multipart/multipart_test.go b/libgo/go/mime/multipart/multipart_test.go new file mode 100644 index 000000000..7e1ed133e --- /dev/null +++ b/libgo/go/mime/multipart/multipart_test.go @@ -0,0 +1,204 @@ +// 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 multipart + +import ( + "bytes" + "fmt" + "io" + "json" + "regexp" + "strings" + "testing" +) + +func TestHorizontalWhitespace(t *testing.T) { + if !onlyHorizontalWhitespace(" \t") { + t.Error("expected pass") + } + if onlyHorizontalWhitespace("foo bar") { + t.Error("expected failure") + } +} + +func TestBoundaryLine(t *testing.T) { + boundary := "myBoundary" + prefix := "--" + boundary + if !isBoundaryDelimiterLine("--myBoundary\r\n", prefix) { + t.Error("expected") + } + if !isBoundaryDelimiterLine("--myBoundary \r\n", prefix) { + t.Error("expected") + } + if !isBoundaryDelimiterLine("--myBoundary \n", prefix) { + t.Error("expected") + } + if isBoundaryDelimiterLine("--myBoundary bogus \n", prefix) { + t.Error("expected fail") + } + if isBoundaryDelimiterLine("--myBoundary bogus--", prefix) { + t.Error("expected fail") + } +} + +func escapeString(v string) string { + bytes, _ := json.Marshal(v) + return string(bytes) +} + +func expectEq(t *testing.T, expected, actual, what string) { + if expected == actual { + return + } + t.Errorf("Unexpected value for %s; got %s (len %d) but expected: %s (len %d)", + what, escapeString(actual), len(actual), escapeString(expected), len(expected)) +} + +func TestFormName(t *testing.T) { + p := new(Part) + p.Header = make(map[string]string) + tests := [...][2]string{ + {`form-data; name="foo"`, "foo"}, + {` form-data ; name=foo`, "foo"}, + {`FORM-DATA;name="foo"`, "foo"}, + {` FORM-DATA ; name="foo"`, "foo"}, + {` FORM-DATA ; name="foo"`, "foo"}, + {` FORM-DATA ; name=foo`, "foo"}, + {` FORM-DATA ; filename="foo.txt"; name=foo; baz=quux`, "foo"}, + } + for _, test := range tests { + p.Header["Content-Disposition"] = test[0] + expected := test[1] + actual := p.FormName() + if actual != expected { + t.Errorf("expected \"%s\"; got: \"%s\"", expected, actual) + } + } +} + +func TestMultipart(t *testing.T) { + testBody := ` +This is a multi-part message. This line is ignored. +--MyBoundary +Header1: value1 +HEADER2: value2 +foo-bar: baz + +My value +The end. +--MyBoundary +Header1: value1b +HEADER2: value2b +foo-bar: bazb + +Line 1 +Line 2 +Line 3 ends in a newline, but just one. + +--MyBoundary + +never read data +--MyBoundary-- +` + testBody = regexp.MustCompile("\n").ReplaceAllString(testBody, "\r\n") + bodyReader := strings.NewReader(testBody) + + reader := NewReader(bodyReader, "MyBoundary") + buf := new(bytes.Buffer) + + // Part1 + part, err := reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part1") + return + } + if part.Header["Header1"] != "value1" { + t.Error("Expected Header1: value") + } + if part.Header["foo-bar"] != "baz" { + t.Error("Expected foo-bar: baz") + } + buf.Reset() + io.Copy(buf, part) + expectEq(t, "My value\r\nThe end.", + buf.String(), "Value of first part") + + // Part2 + part, err = reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part2") + return + } + if part.Header["foo-bar"] != "bazb" { + t.Error("Expected foo-bar: bazb") + } + buf.Reset() + io.Copy(buf, part) + expectEq(t, "Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n", + buf.String(), "Value of second part") + + // Part3 + part, err = reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part3 without errors") + return + } + + // Non-existent part4 + part, err = reader.NextPart() + if part != nil { + t.Error("Didn't expect a third part.") + } + if err != nil { + t.Errorf("Unexpected error getting third part: %v", err) + } +} + +func TestVariousTextLineEndings(t *testing.T) { + tests := [...]string{ + "Foo\nBar", + "Foo\nBar\n", + "Foo\r\nBar", + "Foo\r\nBar\r\n", + "Foo\rBar", + "Foo\rBar\r", + "\x00\x01\x02\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10", + } + + for testNum, expectedBody := range tests { + body := "--BOUNDARY\r\n" + + "Content-Disposition: form-data; name=\"value\"\r\n" + + "\r\n" + + expectedBody + + "\r\n--BOUNDARY--\r\n" + bodyReader := strings.NewReader(body) + + reader := NewReader(bodyReader, "BOUNDARY") + buf := new(bytes.Buffer) + part, err := reader.NextPart() + if part == nil { + t.Errorf("Expected a body part on text %d", testNum) + continue + } + if err != nil { + t.Errorf("Unexpected error on text %d: %v", testNum, err) + continue + } + written, err := io.Copy(buf, part) + expectEq(t, expectedBody, buf.String(), fmt.Sprintf("test %d", testNum)) + if err != nil { + t.Errorf("Error copying multipart; bytes=%v, error=%v", written, err) + } + + part, err = reader.NextPart() + if part != nil { + t.Errorf("Unexpected part in test %d", testNum) + } + if err != nil { + t.Errorf("Unexpected error in test %d: %v", testNum, err) + } + + } +} diff --git a/libgo/go/mime/test.types b/libgo/go/mime/test.types new file mode 100644 index 000000000..9b040edd7 --- /dev/null +++ b/libgo/go/mime/test.types @@ -0,0 +1,8 @@ +# 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. + + + # mime package test +application/test t1 # Simple test +text/test t2 # Text test diff --git a/libgo/go/mime/type.go b/libgo/go/mime/type.go new file mode 100644 index 000000000..a10b780ae --- /dev/null +++ b/libgo/go/mime/type.go @@ -0,0 +1,104 @@ +// 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 mime package implements parts of the MIME spec. +package mime + +import ( + "bufio" + "os" + "strings" + "sync" +) + +var typeFiles = []string{ + "/etc/mime.types", + "/etc/apache2/mime.types", + "/etc/apache/mime.types", +} + +var mimeTypes = map[string]string{ + ".css": "text/css", + ".gif": "image/gif", + ".htm": "text/html; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".jpg": "image/jpeg", + ".js": "application/x-javascript", + ".pdf": "application/pdf", + ".png": "image/png", + ".xml": "text/xml; charset=utf-8", +} + +var mimeLock sync.RWMutex + +func loadMimeFile(filename string) { + f, err := os.Open(filename, os.O_RDONLY, 0666) + if err != nil { + return + } + + reader := bufio.NewReader(f) + for { + line, err := reader.ReadString('\n') + if err != nil { + f.Close() + return + } + fields := strings.Fields(line) + if len(fields) <= 1 || fields[0][0] == '#' { + continue + } + typename := fields[0] + if strings.HasPrefix(typename, "text/") { + typename += "; charset=utf-8" + } + for _, ext := range fields[1:] { + if ext[0] == '#' { + break + } + mimeTypes["."+ext] = typename + } + } +} + +func initMime() { + for _, filename := range typeFiles { + loadMimeFile(filename) + } +} + +var once sync.Once + +// TypeByExtension returns the MIME type associated with the file extension ext. +// The extension ext should begin with a leading dot, as in ".html". +// When ext has no associated type, TypeByExtension returns "". +// +// The built-in table is small but is is augmented by the local +// system's mime.types file(s) if available under one or more of these +// names: +// +// /etc/mime.types +// /etc/apache2/mime.types +// /etc/apache/mime.types +func TypeByExtension(ext string) string { + once.Do(initMime) + mimeLock.RLock() + typename := mimeTypes[ext] + mimeLock.RUnlock() + return typename +} + +// AddExtensionType sets the MIME type associated with +// the extension ext to typ. The extension should begin with +// a leading dot, as in ".html". +func AddExtensionType(ext, typ string) os.Error { + once.Do(initMime) + if len(ext) < 1 || ext[0] != '.' { + return os.EINVAL + } + mimeLock.Lock() + mimeTypes[ext] = typ + mimeLock.Unlock() + return nil +} |