Magic Number

微笑.jpg,这里年轻人们聊天时经常使用的说法,但是!作为一个严谨的程序员,我们能不能通过文件的后缀来判断这个文件的类型呢?例如看到电脑上存在“微笑.jpg”这个文件,通过后缀 jpg 来判断它就是一个 JPEG 文件呢,答案肯定是否定的,因为文件名可以被轻松篡改,例如这个文件本来是“微笑.gif”,有人想不开就将它改成“微笑.jpg”,虽然没有什么影响,但是可以说明一个问题,我们不能通过文件后缀来判断文件的类型。

如果不使用文件后缀,我们应该如何判断文件的类型呢,答案就是我们今天要讨论的Magic Number ,即魔术数,因为文件的本质就是字节数组,所以在一般的操作系统中,对于特定类型的文件,其开头的几个字节都是相同的,例如对于 JPEG 格式的文件,开头的字节都是 \xff\xd8\xff ,我们可以通过读取文件开头的字节来确定文件的类型,这种方法准确性更高。

下面我们准备一个魔术数到类型的字典,注意,这里只列举了几个例子,还可以按照这样的格式增加字典项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var magicNumber = map[string]string {
"\xff\xd8\xff": "image/jpeg",
"\x89PNG\r\n\x1a\n": "image/png",
"GIF87a": "image/gif",
"GIF89a": "image/gif",
}

func getFileType(incipit []byte) string {
incipitStr := string(incipit)
for magic, mime := range magicNumber {
if strings.HasPrefix(incipitStr, magic) {
return mime
}
}
return ""
}

真正匹配格式的逻辑比较简单,遍历 magicNumber 这个 map ,判断每个 key 是不是某个字符串的前缀,因为魔术数符合前缀编码的特点,即任何一个编码都不是另一个编码的前缀,所以我们能保证判断文件类型时,不会有二义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
filepath := os.Args[1]
file, err := os.Open(filepath)
if err != nil {
panic(err)
}
defer file.Close()

buff := make([]byte, 512)
_, err = file.Read(buff)
if err != nil {
panic(err)
}

file.Seek(0, os.SEEK_SET)
typ := getFileType(buff)
fmt.Println(typ)
}

gate upload

下面我们来看一个 Magic Number 的应用场景,例如我们有一个图片上传程序,但是只能允许上传 JPEG 格式的文件,给定一个 io.Reader , 我们先读取前两个字节,判断其值是否等于 JPEGMagic Number ,如果不等,则直接停止上传,如果相等,则继续上传。由于此时 r 已经读取了两个字节了,所以需要重新构造一个 io.Readerr := io.MultiReader(bytes.NewReader(b), r) )继续上传。利用 Magic Number 我们可以避免将整个文件读入内存中,增加程序的运行效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func handleUpload(r io.Reader) error {
b := make([]byte, 2)
_, err := r.Read(b)

if err != nil {
return err
}

jpeg := []byte{0xFF, 0xD8}
if bytes.Equal(b, jprg) {
return error.New("not a JPEG")
}

r := io.MultiReader(bytes.NewReader(b), r)

err = uploadFile(r)
if err != nil {
return err
}

return nil
}
Pieces of Valuable Programming Knowledges