File: ./data.go
   1 package filetypes
   2 
   3 // all the MIME types used/recognized in this package
   4 const (
   5     aiff    = `audio/aiff`
   6     au      = `audio/basic`
   7     avi     = `video/avi`
   8     avif    = `image/avif`
   9     bmp     = `image/x-bmp`
  10     caf     = `audio/x-caf`
  11     cur     = `image/vnd.microsoft.icon`
  12     css     = `text/css`
  13     csv     = `text/csv`
  14     djvu    = `image/x-djvu`
  15     elf     = `application/x-elf`
  16     exe     = `application/vnd.microsoft.portable-executable`
  17     flac    = `audio/x-flac`
  18     gif     = `image/gif`
  19     gz      = `application/gzip`
  20     heic    = `image/heic`
  21     htm     = `text/html`
  22     html    = `text/html`
  23     ico     = `image/x-icon`
  24     iso     = `application/octet-stream`
  25     jpg     = `image/jpeg`
  26     jpeg    = `image/jpeg`
  27     js      = `application/javascript`
  28     json    = `application/json`
  29     m4a     = `audio/aac`
  30     m4v     = `video/x-m4v`
  31     mid     = `audio/midi`
  32     mov     = `video/quicktime`
  33     mp4     = `video/mp4`
  34     mp3     = `audio/mpeg`
  35     mpg     = `video/mpeg`
  36     ogg     = `audio/ogg`
  37     opus    = `audio/opus`
  38     pdf     = `application/pdf`
  39     png     = `image/png`
  40     ps      = `application/postscript`
  41     psd     = `image/vnd.adobe.photoshop`
  42     rtf     = `application/rtf`
  43     sqlite3 = `application/x-sqlite3`
  44     svg     = `image/svg+xml`
  45     text    = `text/plain`
  46     tiff    = `image/tiff`
  47     tsv     = `text/tsv`
  48     wasm    = `application/wasm`
  49     wav     = `audio/x-wav`
  50     webp    = `image/webp`
  51     webm    = `video/webm`
  52     xml     = `application/xml`
  53     zip     = `application/zip`
  54     zst     = `application/zstd`
  55 )
  56 
  57 // type2mime turns dotless format-names into MIME types
  58 var type2mime = map[string]string{
  59     `aiff`:    aiff,
  60     `wav`:     wav,
  61     `avi`:     avi,
  62     `jpg`:     jpg,
  63     `jpeg`:    jpeg,
  64     `m4a`:     m4a,
  65     `mp4`:     mp4,
  66     `m4v`:     m4v,
  67     `mov`:     mov,
  68     `png`:     png,
  69     `avif`:    avif,
  70     `webp`:    webp,
  71     `gif`:     gif,
  72     `tiff`:    tiff,
  73     `psd`:     psd,
  74     `flac`:    flac,
  75     `webm`:    webm,
  76     `mpg`:     mpg,
  77     `zip`:     zip,
  78     `gz`:      gz,
  79     `zst`:     zst,
  80     `mp3`:     mp3,
  81     `opus`:    opus,
  82     `bmp`:     bmp,
  83     `mid`:     mid,
  84     `ogg`:     ogg,
  85     `html`:    html,
  86     `htm`:     htm,
  87     `svg`:     svg,
  88     `xml`:     xml,
  89     `rtf`:     rtf,
  90     `pdf`:     pdf,
  91     `ps`:      ps,
  92     `au`:      au,
  93     `ico`:     ico,
  94     `cur`:     cur,
  95     `caf`:     caf,
  96     `heic`:    heic,
  97     `sqlite3`: sqlite3,
  98     `elf`:     elf,
  99     `exe`:     exe,
 100     `wasm`:    wasm,
 101     `iso`:     iso,
 102     `txt`:     text,
 103     `css`:     css,
 104     `csv`:     csv,
 105     `tsv`:     tsv,
 106     `js`:      js,
 107     `json`:    json,
 108     `geojson`: json,
 109 }
 110 
 111 // formatDescriptor ties a file-header pattern to its data-format type
 112 type formatDescriptor struct {
 113     Header []byte
 114     Type   string
 115 }
 116 
 117 // can be anything: ensure this value differs from all other literal bytes
 118 // in the generic-headers table: failing that, its value could cause subtle
 119 // type-misdetection bugs
 120 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
 121 
 122 // dash-streamed m4a format
 123 var m4aDash = []byte{
 124     cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 125     000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 126 }
 127 
 128 // format markers with leading wildcards, which should be checked before the
 129 // normal ones: this is to prevent mismatches with the latter types, even
 130 // though you can make probabilistic arguments which suggest these mismatches
 131 // should be very unlikely in practice
 132 var specialHeaders = []formatDescriptor{
 133     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 134     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 135     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 136     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 137     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 138     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 139     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 140     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 141     {m4aDash, m4a},
 142 }
 143 
 144 // sqlite3 database format
 145 var sqlite3db = []byte{
 146     'S', 'Q', 'L', 'i', 't', 'e', ' ',
 147     'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
 148     000,
 149 }
 150 
 151 // windows-variant bitmap file-header, which is followed by a byte-counter for
 152 // the 40-byte infoheader which follows that
 153 var winbmp = []byte{
 154     'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
 155 }
 156 
 157 // deja-vu document format
 158 var djv = []byte{
 159     'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
 160 }
 161 
 162 var doctypeHTML = []byte{
 163     '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l', '>',
 164 }
 165 
 166 // hdrDispatch groups format-description-groups by their first byte, thus
 167 // shortening total lookups for some data header: notice how the `ftyp` data
 168 // formats aren't handled here, since these can start with any byte, instead
 169 // of the literal value of the any-byte markers they use
 170 var hdrDispatch = [256][]formatDescriptor{
 171     {
 172         {[]byte{000, 000, 001, 0xBA}, mpg},
 173         {[]byte{000, 000, 001, 0xB3}, mpg},
 174         {[]byte{000, 000, 001, 000}, ico},
 175         {[]byte{000, 000, 002, 000}, cur},
 176         {[]byte{000, 'a', 's', 'm'}, wasm},
 177     }, // 0
 178     nil, // 1
 179     nil, // 2
 180     nil, // 3
 181     nil, // 4
 182     nil, // 5
 183     nil, // 6
 184     nil, // 7
 185     nil, // 8
 186     nil, // 9
 187     nil, // 10
 188     nil, // 11
 189     nil, // 12
 190     nil, // 13
 191     nil, // 14
 192     nil, // 15
 193     nil, // 16
 194     nil, // 17
 195     nil, // 18
 196     nil, // 19
 197     nil, // 20
 198     nil, // 21
 199     nil, // 22
 200     nil, // 23
 201     nil, // 24
 202     nil, // 25
 203     {
 204         {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
 205     }, // 26
 206     nil, // 27
 207     nil, // 28
 208     nil, // 29
 209     nil, // 30
 210     {
 211         // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
 212         {[]byte{0x1F, 0x8B, 0x08}, gz},
 213     }, // 31
 214     nil, // 32
 215     nil, // 33 !
 216     nil, // 34 "
 217     {
 218         {[]byte{'#', '!', ' '}, text},
 219         {[]byte{'#', '!', '/'}, text},
 220     }, // 35 #
 221     nil, // 36 $
 222     {
 223         {[]byte{'%', 'P', 'D', 'F'}, pdf},
 224         {[]byte{'%', '!', 'P', 'S'}, ps},
 225     }, // 37 %
 226     nil, // 38 &
 227     nil, // 39 '
 228     {
 229         {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
 230     }, // 40 (
 231     nil, // 41 )
 232     nil, // 42 *
 233     nil, // 43 +
 234     nil, // 44 ,
 235     nil, // 45 -
 236     {
 237         {[]byte{'.', 's', 'n', 'd'}, au},
 238     }, // 46 .
 239     nil, // 47 /
 240     nil, // 48 0
 241     nil, // 49 1
 242     nil, // 50 2
 243     nil, // 51 3
 244     nil, // 52 4
 245     nil, // 53 5
 246     nil, // 54 6
 247     nil, // 55 7
 248     {
 249         {[]byte{'8', 'B', 'P', 'S'}, psd},
 250     }, // 56 8
 251     nil, // 57 9
 252     nil, // 58 :
 253     nil, // 59 ;
 254     {
 255         // func checkDoc is better for these, since it's case-insensitive
 256         {doctypeHTML, html},
 257         {[]byte{'<', 's', 'v', 'g'}, svg},
 258         {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
 259         {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
 260         {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
 261         {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
 262     }, // 60 <
 263     nil, // 61 =
 264     nil, // 62 >
 265     nil, // 63 ?
 266     nil, // 64 @
 267     {
 268         {djv, djvu},
 269     }, // 65 A
 270     {
 271         {winbmp, bmp},
 272     }, // 66 B
 273     nil, // 67 C
 274     nil, // 68 D
 275     nil, // 69 E
 276     {
 277         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 278         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 279     }, // 70 F
 280     {
 281         {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
 282         {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
 283     }, // 71 G
 284     nil, // 72 H
 285     {
 286         {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 287         {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 288         {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 289         {[]byte{'I', 'I', '*', 000}, tiff},
 290     }, // 73 I
 291     nil, // 74 J
 292     nil, // 75 K
 293     nil, // 76 L
 294     {
 295         {[]byte{'M', 'M', 000, '*'}, tiff},
 296         {[]byte{'M', 'T', 'h', 'd'}, mid},
 297         {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
 298         // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
 299         // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
 300         // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
 301     }, // 77 M
 302     nil, // 78 N
 303     {
 304         {[]byte{'O', 'g', 'g', 'S'}, ogg},
 305     }, // 79 O
 306     {
 307         {[]byte{'P', 'K', 003, 004}, zip},
 308     }, // 80 P
 309     nil, // 81 Q
 310     {
 311         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
 312         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
 313         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
 314     }, // 82 R
 315     {
 316         {sqlite3db, sqlite3},
 317     }, // 83 S
 318     nil, // 84 T
 319     nil, // 85 U
 320     nil, // 86 V
 321     nil, // 87 W
 322     nil, // 88 X
 323     nil, // 89 Y
 324     nil, // 90 Z
 325     nil, // 91 [
 326     nil, // 92 \
 327     nil, // 93 ]
 328     nil, // 94 ^
 329     nil, // 95 _
 330     nil, // 96 `
 331     nil, // 97 a
 332     nil, // 98 b
 333     {
 334         {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
 335     }, // 99 c
 336     nil, // 100 d
 337     nil, // 101 e
 338     {
 339         {[]byte{'f', 'L', 'a', 'C'}, flac},
 340     }, // 102 f
 341     nil, // 103 g
 342     nil, // 104 h
 343     nil, // 105 i
 344     nil, // 106 j
 345     nil, // 107 k
 346     nil, // 108 l
 347     nil, // 109 m
 348     nil, // 110 n
 349     nil, // 111 o
 350     nil, // 112 p
 351     nil, // 113 q
 352     nil, // 114 r
 353     nil, // 115 s
 354     nil, // 116 t
 355     nil, // 117 u
 356     nil, // 118 v
 357     nil, // 119 w
 358     nil, // 120 x
 359     nil, // 121 y
 360     nil, // 122 z
 361     {
 362         {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
 363     }, // 123 {
 364     nil, // 124 |
 365     nil, // 125 }
 366     nil, // 126
 367     {
 368         {[]byte{127, 'E', 'L', 'F'}, elf},
 369     }, // 127
 370     nil, // 128
 371     nil, // 129
 372     nil, // 130
 373     nil, // 131
 374     nil, // 132
 375     nil, // 133
 376     nil, // 134
 377     nil, // 135
 378     nil, // 136
 379     {
 380         {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
 381     }, // 137
 382     nil, // 138
 383     nil, // 139
 384     nil, // 140
 385     nil, // 141
 386     nil, // 142
 387     nil, // 143
 388     nil, // 144
 389     nil, // 145
 390     nil, // 146
 391     nil, // 147
 392     nil, // 148
 393     nil, // 149
 394     nil, // 150
 395     nil, // 151
 396     nil, // 152
 397     nil, // 153
 398     nil, // 154
 399     nil, // 155
 400     nil, // 156
 401     nil, // 157
 402     nil, // 158
 403     nil, // 159
 404     nil, // 160
 405     nil, // 161
 406     nil, // 162
 407     nil, // 163
 408     nil, // 164
 409     nil, // 165
 410     nil, // 166
 411     nil, // 167
 412     nil, // 168
 413     nil, // 169
 414     nil, // 170
 415     nil, // 171
 416     nil, // 172
 417     nil, // 173
 418     nil, // 174
 419     nil, // 175
 420     nil, // 176
 421     nil, // 177
 422     nil, // 178
 423     nil, // 179
 424     nil, // 180
 425     nil, // 181
 426     nil, // 182
 427     nil, // 183
 428     nil, // 184
 429     nil, // 185
 430     nil, // 186
 431     nil, // 187
 432     nil, // 188
 433     nil, // 189
 434     nil, // 190
 435     nil, // 191
 436     nil, // 192
 437     nil, // 193
 438     nil, // 194
 439     nil, // 195
 440     nil, // 196
 441     nil, // 197
 442     nil, // 198
 443     nil, // 199
 444     nil, // 200
 445     nil, // 201
 446     nil, // 202
 447     nil, // 203
 448     nil, // 204
 449     nil, // 205
 450     nil, // 206
 451     nil, // 207
 452     nil, // 208
 453     nil, // 209
 454     nil, // 210
 455     nil, // 211
 456     nil, // 212
 457     nil, // 213
 458     nil, // 214
 459     nil, // 215
 460     nil, // 216
 461     nil, // 217
 462     nil, // 218
 463     nil, // 219
 464     nil, // 220
 465     nil, // 221
 466     nil, // 222
 467     nil, // 223
 468     nil, // 224
 469     nil, // 225
 470     nil, // 226
 471     nil, // 227
 472     nil, // 228
 473     nil, // 229
 474     nil, // 230
 475     nil, // 231
 476     nil, // 232
 477     nil, // 233
 478     nil, // 234
 479     nil, // 235
 480     nil, // 236
 481     nil, // 237
 482     nil, // 238
 483     nil, // 239
 484     nil, // 240
 485     nil, // 241
 486     nil, // 242
 487     nil, // 243
 488     nil, // 244
 489     nil, // 245
 490     nil, // 246
 491     nil, // 247
 492     nil, // 248
 493     nil, // 249
 494     nil, // 250
 495     nil, // 251
 496     nil, // 252
 497     nil, // 253
 498     nil, // 254
 499     {
 500         {[]byte{0xFF, 0xD8, 0xFF}, jpg},
 501         {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
 502         {[]byte{0xFF, 0xFB}, mp3},
 503     }, // 255
 504 }

     File: ./data_test.go
   1 package filetypes
   2 
   3 import (
   4     "strconv"
   5     "testing"
   6 )
   7 
   8 func TestData(t *testing.T) {
   9     t.Run(`could-be-anything constant`, func(t *testing.T) {
  10         if len(hdrDispatch[cba]) != 0 {
  11             const fs = `chosen constant %d collides with header entries`
  12             t.Fatalf(fs, cba)
  13         }
  14     })
  15 
  16     for i, v := range hdrDispatch {
  17         t.Run(`dispatch @ `+strconv.Itoa(i), func(t *testing.T) {
  18             const fs = `expected leading byte to be %d, but got %d instead`
  19             for _, e := range v {
  20                 if e.Header[0] != byte(i) {
  21                     t.Fatalf(fs, i, e.Header[0])
  22                     return
  23                 }
  24             }
  25         })
  26     }
  27 }

     File: ./doc.go
   1 /*
   2 # filetypes
   3 
   4 This package mostly encodes heuristic knowledge to auto-detect file/data types
   5 from the first few bytes: the type can be detected either as a MIME type or as
   6 a dotless extension. As things currently are, 24 bytes are enough to detect
   7 any supported type header/signature, and even 16 are enough for almost all.
   8 
   9 func NameToMIME(fname string) (mimeType string, ok bool)
  10 
  11 NameToMime gives back a MIME-type string from a filename/extension: when that
  12 fails, the latter return value is false.
  13 
  14 func DetectMIME(b []byte) (mimeType string, ok bool)
  15 
  16 DetectMIME gives back a MIME-type string from the leading bytes given: when
  17 that fails, the latter return value is false.
  18 
  19 func DetectType(b []byte) (dotlessExt string, ok bool)
  20 
  21 DetectType gives back a dotless extension-like string: when that fails, the
  22 latter return value is false.
  23 */
  24 package filetypes

     File: ./filetypes.go
   1 package filetypes
   2 
   3 import "bytes"
   4 
   5 // NameToMIME tries to match a MIME type to a filename, dotted file extension,
   6 // or a dot-less filetype/extension given.
   7 func NameToMIME(fname string) (mimeType string, ok bool) {
   8     // handle dotless file types and filenames alike
   9     kind, ok := type2mime[makeDotless(fname)]
  10     return kind, ok
  11 }
  12 
  13 // DetectMIME guesses the first appropriate MIME type from the first few
  14 // data bytes given: 24 bytes are enough to detect all supported types.
  15 func DetectMIME(b []byte) (mimeType string, ok bool) {
  16     t, ok := DetectType(b)
  17     if ok {
  18         return t, true
  19     }
  20     return ``, false
  21 }
  22 
  23 // DetectType guesses the first appropriate file type for the data given:
  24 // here the type is a a filename extension without the leading dot.
  25 func DetectType(b []byte) (dotlessExt string, ok bool) {
  26     // empty data, so there's no way to detect anything
  27     if len(b) == 0 {
  28         return ``, false
  29     }
  30 
  31     // check for plain-text web-document formats case-insensitively
  32     kind, ok := checkDoc(b)
  33     if ok {
  34         return kind, true
  35     }
  36 
  37     // check data formats which allow any byte at the start
  38     kind, ok = checkSpecial(b)
  39     if ok {
  40         return kind, true
  41     }
  42 
  43     // check all other supported data formats
  44     headers := hdrDispatch[b[0]]
  45     for _, t := range headers {
  46         if hasPrefixPattern(b[1:], t.Header[1:], cba) {
  47             return t.Type, true
  48         }
  49     }
  50 
  51     // unrecognized data format
  52     return ``, false
  53 }
  54 
  55 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
  56 // XML, or JSON data
  57 func checkDoc(b []byte) (kind string, ok bool) {
  58     // ignore leading whitespaces
  59     b = trimLeadingWhitespace(b)
  60 
  61     // can't detect anything with empty data
  62     if len(b) == 0 {
  63         return ``, false
  64     }
  65 
  66     // handle HTML/SVG/XML documents
  67     if hasPrefixByte(b, '<') {
  68         if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
  69             if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
  70                 return svg, true
  71             }
  72             return xml, true
  73         }
  74 
  75         headers := hdrDispatch['<']
  76         for _, v := range headers {
  77             if hasPrefixFold(b, v.Header) {
  78                 return v.Type, true
  79             }
  80         }
  81         return ``, false
  82     }
  83 
  84     // handle JSON with top-level arrays
  85     if hasPrefixByte(b, '[') {
  86         // match [", or [[, or [{, ignoring spaces between
  87         b = trimLeadingWhitespace(b[1:])
  88         if len(b) > 0 {
  89             switch b[0] {
  90             case '"', '[', '{':
  91                 return json, true
  92             }
  93         }
  94         return ``, false
  95     }
  96 
  97     // handle JSON with top-level objects
  98     if hasPrefixByte(b, '{') {
  99         // match {", ignoring spaces between: after {, the only valid syntax
 100         // which can follow is the opening quote for the expected object-key
 101         b = trimLeadingWhitespace(b[1:])
 102         if hasPrefixByte(b, '"') {
 103             return json, true
 104         }
 105         return ``, false
 106     }
 107 
 108     // checking for a quoted string, any of the JSON keywords, or even a
 109     // number seems too ambiguous to declare the data valid JSON
 110 
 111     // no web-document format detected
 112     return ``, false
 113 }
 114 
 115 // checkSpecial handles special file-format headers, which should be checked
 116 // before the normal file-type headers, since the first-byte dispatch algo
 117 // doesn't work for these
 118 func checkSpecial(b []byte) (kind string, ok bool) {
 119     if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
 120         for _, t := range specialHeaders {
 121             if hasPrefixPattern(b[4:], t.Header[4:], cba) {
 122                 return t.Type, true
 123             }
 124         }
 125     }
 126     return ``, false
 127 }
 128 
 129 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
 130 // value to signal any byte is allowed on specific spots
 131 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
 132     // if the data are shorter than the pattern to match, there's no match
 133     if len(what) < len(pat) {
 134         return false
 135     }
 136 
 137     // use a slice which ensures the pattern length is never exceeded
 138     what = what[:len(pat)]
 139 
 140     for i, x := range what {
 141         y := pat[i]
 142         if x != y && y != wildcard {
 143             return false
 144         }
 145     }
 146     return true
 147 }

     File: ./filetypes_test.go
   1 package filetypes
   2 
   3 import (
   4     "bytes"
   5     "testing"
   6 )
   7 
   8 func TestCheckDoc(t *testing.T) {
   9     const (
  10         lf       = "\n"
  11         crlf     = "\r\n"
  12         tab      = "\t"
  13         xmlIntro = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`
  14     )
  15 
  16     tests := []struct {
  17         Input    string
  18         Expected string
  19     }{
  20         {``, ``},
  21         {`{"abc":123}`, json},
  22         {`[` + lf + ` {"abc":123}`, json},
  23         {`[` + lf + `  {"abc":123}`, json},
  24         {`[` + crlf + tab + `{"abc":123}`, json},
  25 
  26         {``, ``},
  27         {`<?xml?>`, xml},
  28         {`<?xml?><records>`, xml},
  29         {`<?xml?>` + lf + `<records>`, xml},
  30         {`<?xml?><svg>`, svg},
  31         {`<?xml?>` + crlf + `<svg>`, svg},
  32         {xmlIntro + lf + `<svg`, svg},
  33         {xmlIntro + crlf + `<svg`, svg},
  34     }
  35 
  36     for _, tc := range tests {
  37         t.Run(tc.Input, func(t *testing.T) {
  38             res, _ := checkDoc([]byte(tc.Input))
  39             if res != tc.Expected {
  40                 t.Fatalf(`got %v, expected %v instead`, res, tc.Expected)
  41             }
  42         })
  43     }
  44 }
  45 
  46 func TestHasPrefixPattern(t *testing.T) {
  47     var (
  48         data = []byte{
  49             'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
  50         }
  51         pat = []byte{
  52             'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
  53         }
  54     )
  55 
  56     if !hasPrefixPattern(data, pat, cba) {
  57         t.Fatal(`wildcard pattern not working`)
  58     }
  59 }
  60 
  61 func BenchmarkHasPrefixMatch(b *testing.B) {
  62     var (
  63         data = []byte{
  64             'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
  65         }
  66         pat = []byte{
  67             'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
  68         }
  69     )
  70 
  71     b.ReportAllocs()
  72     b.ResetTimer()
  73 
  74     for i := 0; i < b.N; i++ {
  75         if !bytes.HasPrefix(data, pat) {
  76             b.Fatal(`pattern was specifically chosen to match, but didn't`)
  77         }
  78     }
  79 }
  80 
  81 func BenchmarkHasPrefixPatternMatch(b *testing.B) {
  82     var (
  83         data = []byte{
  84             'R', 'I', 'F', 'F', 0xf0, 0xba, 0xc8, 0x2b, 'A', 'V', 'I', ' ',
  85         }
  86         pat = []byte{
  87             'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' ',
  88         }
  89     )
  90 
  91     b.ReportAllocs()
  92     b.ResetTimer()
  93 
  94     for i := 0; i < b.N; i++ {
  95         if !hasPrefixPattern(data, pat, cba) {
  96             b.Fatal(`pattern was specifically chosen to match, but didn't`)
  97         }
  98     }
  99 }

     File: ./go.mod
   1 module filetypes
   2 
   3 go 1.16

     File: ./strings.go
   1 package filetypes
   2 
   3 import (
   4     "bytes"
   5     "strings"
   6 )
   7 
   8 // makeDotless is similar to filepath.Ext, except its results never start
   9 // with a dot
  10 func makeDotless(s string) string {
  11     i := strings.LastIndexByte(s, '.')
  12     if i >= 0 {
  13         return s[(i + 1):]
  14     }
  15     return s
  16 }
  17 
  18 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
  19 func hasPrefixByte(b []byte, prefix byte) bool {
  20     return len(b) > 0 && b[0] == prefix
  21 }
  22 
  23 // hasPrefixFold is a case-insensitive bytes.HasPrefix
  24 func hasPrefixFold(s []byte, prefix []byte) bool {
  25     n := len(prefix)
  26     return len(s) >= n && bytes.EqualFold(s[:n], prefix)
  27 }
  28 
  29 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
  30 // to handle text-based data formats more flexibly
  31 func trimLeadingWhitespace(b []byte) []byte {
  32     for len(b) > 0 {
  33         switch b[0] {
  34         case ' ', '\t', '\n', '\r':
  35             b = b[1:]
  36         default:
  37             return b
  38         }
  39     }
  40 
  41     // an empty slice is all that's left, at this point
  42     return nil
  43 }

     File: ./strings_test.go
   1 package filetypes
   2 
   3 import (
   4     "bytes"
   5     "testing"
   6 )
   7 
   8 func TestHasPrefixByte(t *testing.T) {
   9     var tests = []struct {
  10         Data     []byte
  11         Prefix   byte
  12         Expected bool
  13     }{
  14         {nil, 'x', false},
  15         {[]byte(`x`), 'x', true},
  16         {[]byte(` x`), 'x', false},
  17         {[]byte(`xyz`), 'a', false},
  18         {[]byte(`abcxyz`), 'a', true},
  19     }
  20 
  21     for _, tc := range tests {
  22         t.Run(string(tc.Data), func(t *testing.T) {
  23             got := hasPrefixByte(tc.Data, tc.Prefix)
  24             if got != tc.Expected {
  25                 const fs = `expected %v, but got %v instead`
  26                 t.Fatalf(fs, tc.Expected, got)
  27             }
  28         })
  29     }
  30 }
  31 
  32 func TestHasPrefixFold(t *testing.T) {
  33     var tests = []struct {
  34         Data     []byte
  35         Prefix   []byte
  36         Expected bool
  37     }{
  38         {[]byte("<!docTYPE html>\n<html>"), []byte(`<!doctype HTML`), true},
  39     }
  40 
  41     for _, tc := range tests {
  42         t.Run("", func(t *testing.T) {
  43             got := hasPrefixFold(tc.Data, tc.Prefix)
  44             if got != tc.Expected {
  45                 const fs = `expected %v, but got %v instead`
  46                 t.Fatalf(fs, tc.Expected, got)
  47             }
  48         })
  49     }
  50 }
  51 
  52 func TestTrimLeadingWhitespaces(t *testing.T) {
  53     var tests = []struct {
  54         Data     []byte
  55         Expected []byte
  56     }{
  57         {[]byte(`abc`), []byte(`abc`)},
  58         {[]byte(" \t"), nil},
  59         {[]byte("  \tabc"), []byte(`abc`)},
  60         {[]byte("\r\nabc"), []byte(`abc`)},
  61     }
  62 
  63     for _, tc := range tests {
  64         t.Run("", func(t *testing.T) {
  65             got := trimLeadingWhitespace(tc.Data)
  66             if !bytes.Equal(got, tc.Expected) {
  67                 const fs = `expected %#v, but got %#v instead`
  68                 t.Fatalf(fs, tc.Expected, got)
  69             }
  70         })
  71     }
  72 }

     File: ./type-headers.txt
   1 Sources
   2 =======
   3 
   4 Got the file-type signatures from
   5     https://en.wikipedia.org/wiki/List_of_file_signatures
   6     https://www.garykessler.net/library/file_sigs.html
   7 
   8 The latter site has info to detect various variants of mp4 files, as well as
   9 mov files.
  10 
  11 
  12 File Signatures
  13 ===============
  14 
  15 png
  16 89 50 4E 47 0D 0A 1A 0A
  17 
  18 gif
  19 47 49 46 38 39 61
  20 
  21 jpg
  22 FF D8 FF E0
  23 FF D8 FF E1
  24 
  25 wav (riff wave)
  26 52 49 46 46 ?? ?? ?? ?? 57 41 56 45
  27 
  28 avi (riff avi)
  29 52 49 46 46 ?? ?? ?? ?? 41 56 49 20
  30 
  31 webp (riff webp)
  32 52 49 46 46 ?? ?? ?? ?? 57 45 42 50
  33 
  34 mp3
  35 FF FB
  36 49 44 33
  37 
  38 bmp
  39 42 4D
  40 
  41 flac
  42 66 4C 61 43
  43 
  44 aiff
  45 46 4F 52 4D ?? ?? ?? ?? 41 49 46 46
  46 
  47 webm / mkv
  48 1A 45 DF A3
  49 
  50 tiff
  51 49 49 2A 00
  52 4D 4D 00 2A
  53 
  54 rtf
  55 7B 5C 72 74 66
  56 
  57 pdf
  58 25 50 44 46
  59 
  60 djvu
  61 41 54 26 54 46 4F 52 4D ?? ?? ?? ?? 44 4A 56
  62 
  63 zip
  64 50 4B 03 04
  65 
  66 mpg
  67 00 00 01 BA
  68 00 00 01 B3
  69 
  70 mp4
  71 ?? ?? ?? ?? 66 74 79 70 4D 53 4E 56
  72 ?? ?? ?? ?? 66 74 79 70 69 73 6F 6D
  73 
  74 m4a
  75 ?? ?? ?? ?? 66 74 79 70 4D 34 41 20
  76 
  77 m4v
  78 ?? ?? ?? ?? 66 74 79 70 6D 70 34 32
  79 
  80 mov
  81 ?? ?? ?? ?? 66 74 79 70 71 74 20 20