Elixir 为处理二进制数据提供了优雅的语法,就像我们见过的、用于处理位串的特殊形式<<>>一样。
二进制类型是位串类型的一种特化。位串的长度可以是任意的(位的数量不限),而二进制数据的位数必须能被 8 整除。也就是说,处理二进制数据时,我们常常以字节(8 位)为单位来思考。一个字节可以表示从0到255的整数。用十六进制来表示字节值很常见,也就是0x00 - 0xFF。
二进制字面量使用位串特殊形式<<>>来定义。定义二进制字面量时,可以使用整数字面量和字符串字面量。大于 255 的整数值会溢出,只会用到该整数最后的 8 位。默认情况下,会为该值应用::binary修饰符。我们可以用<>/2运算符拼接二进制数据。
<<255>> == <<0xFF>>
# Overflowing bits are truncated
<<256>> == <<0>>
<<2, 4, 6, 8, 10, 12, 14, 16>> == <<0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10>>
空字节 是<<0>>的另一个名称。
模式匹配甚至能用在二进制数据上,我们可以像处理数组那样,对二进制数据的一部分进行模式匹配。
# Ignore the first 8 bytes, match and bind the remaining to `body`
<<_::binary-size(8), body::binary>>
和其他类型的模式匹配一样,我们可以在函数签名中使用它,在多个函数子句之间进行选择。
你一直在做一个项目,它允许用户把文件上传到服务器,与其他用户分享。现在你的任务是编写一个函数,用来验证上传的文件是否与它的媒体类型匹配。你查了一些资料,发现文件的前几个字节通常只有该文件类型才有,因此它就像是文件的一种签名。
可以参考下面的表格:
| 文件类型 | 常见扩展名 | 媒体类型 | 二进制“签名” |
|---|---|---|---|
| ELF | "exe" |
"application/octet-stream" |
0x7F, 0x45, 0x4C, 0x46 |
| BMP | "bmp" |
"image/bmp" |
0x42, 0x4D |
| PNG | "png" |
"image/png" |
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A |
| JPG | "jpg" |
"image/jpg" |
0xFF, 0xD8, 0xFF |
| GIF | "gif" |
"image/gif" |
0x47, 0x49, 0x46 |
实现type_from_extension/1函数。它接收一个文件扩展名(字符串),返回对应的媒体类型(字符串);如果该扩展名与预期的都不匹配,就返回 nil。
FileSniffer.type_from_extension("exe")
# => "application/octet-stream"
FileSniffer.type_from_extension("txt")
# => nil
实现type_from_binary/1函数。它接收一个文件(二进制),返回对应的媒体类型(字符串);如果该文件的签名与预期的都不匹配,就返回 nil。
file = File.read!("application.exe")
FileSniffer.type_from_binary(file)
# => "application/octet-stream"
file = File.read!("example.txt")
FileSniffer.type_from_binary(file)
# => nil
不用操心如何把文件读取成二进制。可以认为这一步已经帮你做好了,测试会把它作为实参传进来。
实现verify/2函数。它接收一个文件(二进制)和一个扩展名(字符串),返回:ok或:error元组。
file = File.read!("application.exe")
FileSniffer.verify(file, "exe")
# => {:ok, "application/octet-stream"}
FileSniffer.verify(file, "png")
# => {:error, "Warning, file format and file extension do not match."}