// Copyright 2019 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package media contains Media Type (MIME type) related types and functions. package media import ( "encoding/json" "fmt" "net/http" "strings" ) var zero Type const ( DefaultDelimiter = "." ) // MediaType (also known as MIME type and content type) is a two-part identifier for // file formats and format contents transmitted on the Internet. // For Hugo's use case, we use the top-level type name / subtype name + suffix. // One example would be application/svg+xml // If suffix is not provided, the sub type will be used. // { "name": "MediaType" } type Type struct { // The full MIME type string, e.g. "application/rss+xml". Type string `json:"-"` // The top-level type name, e.g. "application". MainType string `json:"mainType"` // The subtype name, e.g. "rss". SubType string `json:"subType"` // The delimiter before the suffix, e.g. ".". Delimiter string `json:"delimiter"` // FirstSuffix holds the first suffix defined for this MediaType. FirstSuffix SuffixInfo `json:"-"` // This is the optional suffix after the "+" in the MIME type, // e.g. "xml" in "application/rss+xml". mimeSuffix string // E.g. "jpg,jpeg" // Stored as a string to make Type comparable. // For internal use only. SuffixesCSV string `json:"-"` } // SuffixInfo holds information about a Media Type's suffix. type SuffixInfo struct { // Suffix is the suffix without the delimiter, e.g. "xml". Suffix string `json:"suffix"` // FullSuffix is the suffix with the delimiter, e.g. ".xml". FullSuffix string `json:"fullSuffix"` } // FromContent resolve the Type primarily using http.DetectContentType. // If http.DetectContentType resolves to application/octet-stream, a zero Type is returned. // If http.DetectContentType resolves to text/plain or application/xml, we try to get more specific using types and ext. func FromContent(types Types, extensionHints []string, content []byte) Type { t := strings.Split(http.DetectContentType(content), ";")[0] if t == "application/octet-stream" { return zero } var found bool m, found := types.GetByType(t) if !found { if t == "text/xml" { // This is how it's configured in Hugo by default. m, found = types.GetByType("application/xml") } } if !found { return zero } var mm Type for _, extension := range extensionHints { extension = strings.TrimPrefix(extension, ".") mm, _, found = types.GetFirstBySuffix(extension) if found { break } } if found { if m == mm { return m } if m.IsText() && mm.IsText() { // http.DetectContentType isn't brilliant when it comes to common text formats, so we need to do better. // For now we say that if it's detected to be a text format and the extension/content type in header reports // it to be a text format, then we use that. return mm } // E.g. an image with a *.js extension. return zero } return m } // FromStringAndExt creates a Type from a MIME string and a given extensions func FromStringAndExt(t string, ext ...string) (Type, error) { tp, err := FromString(t) if err != nil { return tp, err } for i, e := range ext { ext[i] = strings.TrimPrefix(e, ".") } tp.SuffixesCSV = strings.Join(ext, ",") tp.Delimiter = DefaultDelimiter tp.init() return tp, nil } // FromString creates a new Type given a type string on the form MainType/SubType and // an optional suffix, e.g. "text/html" or "text/html+html". func FromString(t string) (Type, error) { t = strings.ToLower(t) parts := strings.Split(t, "/") if len(parts) != 2 { return Type{}, fmt.Errorf("cannot parse %q as a media type", t) } mainType := parts[0] subParts := strings.Split(parts[1], "+") subType := strings.Split(subParts[0], ";")[0] var suffix string if len(subParts) > 1 { suffix = subParts[1] } var typ string if suffix != "" { typ = mainType + "/" + subType + "+" + suffix } else { typ = mainType + "/" + subType } return Type{Type: typ, MainType: mainType, SubType: subType, mimeSuffix: suffix}, nil } // For internal use. func (m Type) String() string { return m.Type } // Suffixes returns all valid file suffixes for this type. func (m Type) Suffixes() []string { if m.SuffixesCSV == "" { return nil } return strings.Split(m.SuffixesCSV, ",") } // IsText returns whether this Type is a text format. // Note that this may currently return false negatives. // TODO(bep) improve // For internal use. func (m Type) IsText() bool { if m.MainType == "text" { return true } switch m.SubType { case "javascript", "json", "rss", "xml", "svg", "toml", "yml", "yaml": return true } return false } // For internal use. func (m Type) IsHTML() bool { return m.SubType == Builtin.HTMLType.SubType } // For internal use. func (m Type) IsMarkdown() bool { return m.SubType == Builtin.MarkdownType.SubType } func InitMediaType(m *Type) { m.init() } func (m *Type) init() { m.FirstSuffix.FullSuffix = "" m.FirstSuffix.Suffix = "" if suffixes := m.Suffixes(); suffixes != nil { m.FirstSuffix.Suffix = suffixes[0] m.FirstSuffix.FullSuffix = m.Delimiter + m.FirstSuffix.Suffix } } func newMediaType(main, sub string, suffixes []string) Type { t := Type{MainType: main, SubType: sub, SuffixesCSV: strings.Join(suffixes, ","), Delimiter: DefaultDelimiter} t.init() return t } func newMediaTypeWithMimeSuffix(main, sub, mimeSuffix string, suffixes []string) Type { mt := newMediaType(main, sub, suffixes) mt.mimeSuffix = mimeSuffix mt.init() return mt } // Types is a slice of media types. // { "name": "MediaTypes" } type Types []Type func (t Types) Len() int { return len(t) } func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] } func (t Types) Less(i, j int) bool { return t[i].Type < t[j].Type } // GetBestMatch returns the best match for the given media type string. func (t Types) GetBestMatch(s string) (Type, bool) { // First try an exact match. if mt, found := t.GetByType(s); found { return mt, true } // Try main type. if mt, found := t.GetBySubType(s); found { return mt, true } // Try extension. if mt, _, found := t.GetFirstBySuffix(s); found { return mt, true } return Type{}, false } // GetByType returns a media type for tp. func (t Types) GetByType(tp string) (Type, bool) { for _, tt := range t { if strings.EqualFold(tt.Type, tp) { return tt, true } } if !strings.Contains(tp, "+") { // Try with the main and sub type parts := strings.Split(tp, "/") if len(parts) == 2 { return t.GetByMainSubType(parts[0], parts[1]) } } return Type{}, false } func (t Types) normalizeSuffix(s string) string { return strings.ToLower(strings.TrimPrefix(s, ".")) } // BySuffix will return all media types matching a suffix. func (t Types) BySuffix(suffix string) []Type { suffix = t.normalizeSuffix(suffix) var types []Type for _, tt := range t { if tt.hasSuffix(suffix) { types = append(types, tt) } } return types } // GetFirstBySuffix will return the first type matching the given suffix. func (t Types) GetFirstBySuffix(suffix string) (Type, SuffixInfo, bool) { suffix = t.normalizeSuffix(suffix) for _, tt := range t { if tt.hasSuffix(suffix) { return tt, SuffixInfo{ FullSuffix: tt.Delimiter + suffix, Suffix: suffix, }, true } } return Type{}, SuffixInfo{}, false } // GetBySuffix gets a media type given as suffix, e.g. "html". // It will return false if no format could be found, or if the suffix given // is ambiguous. // The lookup is case insensitive. func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) { suffix = t.normalizeSuffix(suffix) for _, tt := range t { if tt.hasSuffix(suffix) { if found { // ambiguous found = false return } tp = tt si = SuffixInfo{ FullSuffix: tt.Delimiter + suffix, Suffix: suffix, } found = true } } return } func (t Types) IsTextSuffix(suffix string) bool { suffix = t.normalizeSuffix(suffix) for _, tt := range t { if tt.hasSuffix(suffix) { return tt.IsText() } } return false } func (m Type) hasSuffix(suffix string) bool { return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",") } // GetByMainSubType gets a media type given a main and a sub type e.g. "text" and "plain". // It will return false if no format could be found, or if the combination given // is ambiguous. // The lookup is case insensitive. func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool) { for _, tt := range t { if strings.EqualFold(mainType, tt.MainType) && strings.EqualFold(subType, tt.SubType) { if found { // ambiguous found = false return } tp = tt found = true } } return } // GetBySubType gets a media type given a sub type e.g. "plain". func (t Types) GetBySubType(subType string) (tp Type, found bool) { for _, tt := range t { if strings.EqualFold(subType, tt.SubType) { if found { // ambiguous found = false return } tp = tt found = true } } return } // IsZero reports whether this Type represents a zero value. // For internal use. func (m Type) IsZero() bool { return m.SubType == "" } // MarshalJSON returns the JSON encoding of m. // For internal use. func (m Type) MarshalJSON() ([]byte, error) { type Alias Type return json.Marshal(&struct { Alias Type string `json:"type"` String string `json:"string"` Suffixes []string `json:"suffixes"` }{ Alias: (Alias)(m), Type: m.Type, String: m.String(), Suffixes: strings.Split(m.SuffixesCSV, ","), }) }