// Package sniff decides what an uploaded file IS from its first bytes
// and nothing else (RFC 0016 §Reading, §Inline).
//
// An extension is a name the uploader chose and a mimetype is a card
// they wrote; neither is a fact about the content. Every route that
// answers with a content type a browser will render reads this package
// instead, and the same answer is what the attachment card draws, so
// the preview a reader sees and the bytes the browser gets can never
// disagree about what the file is.
package sniff
import (
"bytes"
"unicode/utf8"
)
// The four kinds an attachment card draws.
const (
KindImage = "image"
KindText = "text"
KindMedia = "media"
KindOther = "other"
)
// Image reports the content type of bytes that BEGIN with one of the
// four raster signatures quarry renders inline, and false for anything
// else. SVG cannot reach it by construction: SVG is script, and no
// magic number admits it.
func Image(raw []byte) (string, bool) {
switch {
case bytes.HasPrefix(raw, []byte("\x89PNG\r\n\x1a\n")):
return "image/png", true
case bytes.HasPrefix(raw, []byte{0xff, 0xd8, 0xff}):
return "image/jpeg", true
case bytes.HasPrefix(raw, []byte("GIF87a")), bytes.HasPrefix(raw, []byte("GIF89a")):
return "image/gif", true
case riff(raw, "WEBP"):
return "image/webp", true
}
return "", false
}
// Media reports the content type of bytes that begin with one of the
// audio or video signatures quarry serves inline, and false otherwise.
//
// These types are handed to a <video> or <audio> element, which decodes
// them with the same codecs an image goes through: a container the
// browser cannot parse plays nothing, and no container here can become
// a document. PDF and SVG stay off the list for the opposite reason —
// both are rendered by an engine that runs script.
func Media(raw []byte) (string, bool) {
switch {
case len(raw) >= 8 && bytes.Equal(raw[4:8], []byte("ftyp")):
return "video/mp4", true
case bytes.HasPrefix(raw, []byte{0x1a, 0x45, 0xdf, 0xa3}):
return "video/webm", true
case bytes.HasPrefix(raw, []byte("OggS")):
// The container says nothing about whether the stream inside is
// audio or video, so the audio type is the safe claim: every
// browser that plays Ogg at all plays it in an <audio>.
return "audio/ogg", true
case bytes.HasPrefix(raw, []byte("ID3")):
return "audio/mpeg", true
case len(raw) >= 2 && raw[0] == 0xff && raw[1]&0xe0 == 0xe0:
return "audio/mpeg", true
case riff(raw, "WAVE"):
return "audio/wav", true
}
return "", false
}
// riff reports whether raw is a RIFF container of the named form. WAV
// and WebP share the same first four bytes, so the form at offset 8 is
// the only thing that separates a sound from a picture.
func riff(raw []byte, form string) bool {
return len(raw) >= 12 && bytes.HasPrefix(raw, []byte("RIFF")) &&
bytes.Equal(raw[8:12], []byte(form))
}
// Text reports whether these bytes can be shown as text: valid UTF-8
// with no NUL anywhere in them. The whole slice is scanned rather than
// a prefix, because a file that reads as prose for a screen and then
// turns binary would otherwise hand a terminal's worth of escape
// sequences to a reader who asked for a preview.
func Text(raw []byte) bool {
if len(raw) == 0 {
return false
}
return utf8.Valid(raw) && !bytes.ContainsRune(raw, 0)
}
// Kind classifies an attachment into the four shapes a card draws.
// Order matters: an image is an image before it is anything else, and
// text is the last claim made, so a container that merely happens to
// be valid UTF-8 is still media.
func Kind(raw []byte) string {
if _, ok := Image(raw); ok {
return KindImage
}
if _, ok := Media(raw); ok {
return KindMedia
}
if Text(raw) {
return KindText
}
return KindOther
}
// TrimPartialRune drops an incomplete UTF-8 sequence from the end of a
// PREFIX of a larger file.
//
// A bounded read cuts wherever the bound falls, which is routinely in
// the middle of a multibyte rune: without this, every UTF-8 document
// whose 65,536th byte lands mid-character would be judged binary. At
// most three bytes go, and only bytes that are a valid START of a rune
// the prefix does not finish — a genuinely invalid tail is left alone,
// so the verdict on real garbage is unchanged.
func TrimPartialRune(prefix []byte) []byte {
for i := len(prefix) - 1; i >= 0 && i >= len(prefix)-3; i-- {
b := prefix[i]
if b&0xc0 == 0x80 {
continue // a continuation byte: keep walking back to its lead
}
if b&0x80 == 0 {
return prefix // ASCII: the tail is complete
}
// A lead byte, and only a REAL one: 0xf8 and up start no rune in
// UTF-8, so bytes like those are invalid wherever the bound
// fell and trimming them would launder garbage into text.
var want int
switch {
case b&0xe0 == 0xc0:
want = 2
case b&0xf0 == 0xe0:
want = 3
case b&0xf8 == 0xf0:
want = 4
default:
return prefix
}
if len(prefix)-i < want {
return prefix[:i]
}
return prefix
}
return prefix
}