在OTA升级中,常常需要对固件进行合法性校验,比如完整性、硬件版本、软件版本等,

这里是一个简单的格式,可用于普通的场景,正式环境中还应该考虑加密等安全措施。

参考格式

固件的头部信息,可以是多种格式,一般可能更多的是二进制数据格式,考虑到后期方便扩展,这里选择使用json格式。

1
2
3
4
5
6
7
8
ABCD{
"magic":"device_xx",
"appVer":"1.0.1",
"hardVer":"1.0.1",
"format":"elf",
"size":1234,
"md5":"2d02e669731cbade6a64b58d602cf2a4"
}

JSON参数说明

ABCD – json数据的长度,2字节,16进制字串格式,如 3456表示0x3456;

magic – 魔数,表示固件的类型,以设备类型_XX表示,其中XX用于区别同一类型设备的不同版本;

appVer – 固件版本号,设备根据版本号决定是否升级;

hardVer – 支持的硬件(最低)版本,设备根据硬件版本信息,判断此固件是否适用,从而决定是否可以升级;

format – 固件格式,主要有如下格式:

格式 说明
elf linux可执行文件格式,主要用于网关
ipk openwrt固件安装包格式,用于网关
bin 二进制格式, 主要用于子设备
hex hex文件格式,主要用于子设备

size – 固件大小(不包含固件头json信息),int型,占4字节,单位byte;

md5 – 固件的MD5校验值, 32位字符串。

相关知识

jq – JSON命令行处理工具

jq简介

jq 是一款命令行下处理 JSON 数据的工具。其可以接受标准输入,命令管道或者文件中的 JSON 数据,经过一系列的过滤器(filters)和表达式的转后形成我们需要的数据结构并将结果输出到标准输出中。jq 的这种特性使我们可以很容易地在 Shell 脚本中调用它。

需要说明的是 jq 只能接受 well form 的 JSON 字符串作为输入内容。也就是说输入内容必须严格遵循 JSON 格式的标准。所有的属性名必须是以双引号包括的字符串。对象的最后一个属性的末尾或者数组的最后一个元素的末尾不能有逗号。否则 jq 会抛出无法解析 JSON 的错误。

jq基本操作

  • 帮助

    1
    jq -h
  • 格式化json

    1
    2
    3
    4
    5
    6
    7
    8
    # -c 删除漂亮的格式输出
    echo '{"url": "mozillazg.com"}' |jq -c .
    {"url":"mozillazg.com"}

    echo '{"url": "mozillazg.com"}' |jq .
    {
    "url": "mozillazg.com"
    }
  • 获取某个key值
    .key, .foo.bar, ["key"]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 如果key对应的值存在,则输出值
    echo '{"url": "mozillazg.com"}' |jq .url
    "mozillazg.com"
    echo '{"url": "mozillazg.com"}' | jq '.["url"]'
    "mozillazg.com"

    # 如果key对应的值不存在,则输出null
    echo '{"notfoo": true, "alsonotfoo": false}' | jq '.foo'
    null

    echo '{"notfoo": true, "alsonotfoo": false}' | jq .foo
    null
  • 数组操作
    .[]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    # 取出所有元素
    echo '[{"name": "tom"}, {"name": "mozillazg"}]' |jq .[]
    {
    "name": "tom"
    }
    {
    "name": "mozillazg"
    }

    # 取出第1个元素,下标从0开始
    echo '[{"name": "tom"}, {"name": "mozillazg"}]' |jq .[0]
    {
    "name": "tom"
    }

    #取出下标为 0 到 2(不包括2)之间的元素
    echo '[{"name": "tom"}, {"name": "mozillazg"}, {"name": "jim"}]' |jq .[0:2]
    [
    {
    "name": "tom"
    },
    {
    "name": "mozillazg"
    }
    ]
  • 取出数组元素中的key的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    echo '[{"name": "foo"},{"name": "bar"},{"name": "foobar"}]' |jq .[].name
    "foo"
    "bar"
    "foobar"

    # 使用管道
    echo '[{"name": "foo"},{"name": "bar"},{"name": "foobar"}]' |jq '.[]|.name'
    "foo"
    "bar"
    "foobar"
  • 将结果重新组成数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    echo '[{"name": "foo"},{"name": "bar"},{"name": "foobar"}]' |jq [.[].name]
    [
    "foo",
    "bar",
    "foobar"
    ]

    # 使用map实现
    echo '[{"name": "foo"},{"name": "bar"},{"name": "foobar"}]' |jq 'map(.name)'
    [
    "foo",
    "bar",
    "foobar"
    ]
  • 处理文件

    使用jq [选项] <jq表达式> [files]形式;
    使用cat [files] | jq [选项] <jq表达式>

jq高级操作

  • 管道

    • 支持管道线|,它如同linux命令中的管道线——把前面命令的输出当作是后面命令的输入。
    1
    2
    echo '{"url": "mozillazg.com", "name": "mozillazg"}' | jq '.|.url'
    "mozillazg.com"
    • 获取内容的长度(字符串,数组的长度)
    1
    2
    3
    4
    5
    echo '{"url": "mozillazg.com", "name": "mozillazg"}' |jq '.url|length'
    13

    echo '["mozillazg.com", "mozillazg"]' |jq '.|length'
    2
  • map
    map(foo)可以实现对数组的每一项进行操作,然后合并结果的功能:

    1
    2
    3
    4
    5
    echo '["mozillazg.com", "mozillazg"]' | jq 'map(length)'
    [
    13,
    9
    ]
  • filter(select)
    select(foo) 可以实现对输入项进行判断,只返回符合条件的项:

    1
    2
    3
    4
    echo '["mozillazg.com", "mozillazg"]' | jq 'map(select(.|length > 9))'
    [
    "mozillazg.com"
    ]
  • 字符串插值,拼接
    可以使用 \(foo) 实现字符串插值功能:

    1
    2
    echo '{"url": "mozillazg.com", "name": "mozillazg"}' |jq '"hi \(.name)"'
    "hi mozillazg"

    注意要用双引号包围起来,表示是一个字符串。

  • if/elif/else
    可以使用 if .. then .. elif .. then .. else .. end 实现条件判断:

    1
    2
    3
    4
    5
    6
    7
    echo '[0, 1, 2, 3]' | jq 'map(if . == 0 then "zero" elif . == 1 then "one" elif . == 2 then "two" else "many" end)'
    [
    "zero",
    "one",
    "two",
    "many"
    ]
  • 构造object或数组
    可以通过 {}[] 构造新的 object 或 数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # object
    echo '["mozillazg.com", "mozillazg"]' |jq '{name: .[1]}'
    {
    "name": "mozillazg"
    }

    # array
    echo '{"url": "mozillazg.com", "name": "mozillazg"}' |jq '[.name, .url]'
    [
    "mozillazg",
    "mozillazg.com"
    ]

jq内置运算支持

jq 内部支持的数据类型有:数字,字符串,数组和对象(object)。并且在这些数据类型的基础上, jq 提供了一些基本的操作符来实现一些基本的运算和数据操作。列举如下:

  • 数学运算。对于数字类型,jq 实现了基本的加减乘除(/)和求余(%)运算。对于除法运算,jq 最多支持 16 位小数。
  • 字符串操作。jq 提供字符串的连接操作(运算符为’+’,例如:”tom “+”jerry”结果为”tom jerry”),字符串的复制操作(例如:’a’*3 结果为’aaa’),以及字符串分割操作(将字符串按照指定的分割符分成数组,例如”sas”/“s”的结果为[“”,”a”,””],而”sas”/“a”的结果为[“s”,”s”]。
  • 数组操作。jq 提供两种数组运算:并集(‘+’)运算,结果数组中包含参与运算的数组的所有元素。差集运算(‘-‘),例如:有数组 a,b, a-b 的结果为所有在 a 中且不包含在 b 中的元素组成的数组。
  • 对象操作。jq 实现了两个 JSON 对象的合并操作(merge)。当两个参与运算的对象包含相同的属性时则保留运算符右侧对象的属性值。有两种合并运算符:’+’和’‘。所不同的是,运算符’+’只做顶层属性的合并,运算符’‘则是递归合并。例如:有对象 a={“a”:{“b”:1}}, b={“a”:{“c”:2}},a+b 的结果为{“a”:{“c”:2}},而 a*b 的结果为{“a”:{“b”:1,”c”:2}}
  • 比较操作:jq 内部支持的比较操作符有==, !=,>,>=,<=和<。其中,’==’的规则和 javascript 中的恒等(‘===’)类似,只有两个操作数的类型和值均相同时其结果才是 true。
  • 逻辑运算符: and/or/not。在 jq 逻辑运算中,除了 false 和 null 外,其余的任何值都等同于 true。
  • 默认操作符(‘//‘), 表达式’a//b’表示当表达式 a 的值不是 false 或 null 时,a//b 等于 a,否则等于 b。

jq修改json数据

原json内容:

例如需要修改size的值为123,方式如下:

1
2
3
4
5
6
7
8
#重定向到新的文件,注意不能是原文件
cat firmware.json | jq 'to_entries |
map(if .key == "size"
then . + {"value":123}
else .
end
) |
from_entries' > new.json

参考引用

https://www.ibm.com/developerworks/cn/linux/1612_chengg_jq/index.html

https://mozillazg.com/2018/01/jq-use-examples-cookbook.html#hidid4

https://github.com/meetbill/xbatch/wiki/jq#22-%E6%A0%B9%E6%8D%AE-key-%E6%9F%A5%E8%AF%A2-json-%E7%9A%84%E5%80%BC

https://www.jianshu.com/p/f50c87b7eaea

shell获取文件的大小

1
2
3
4
5
ls -l filename | awk '{print $5}'
du -b filename | awk '{print $1}'
wc -c filename | awk '{print $1}'
wc -c < filename
stat -c "%s" filename

shell计算文件的MD5

md5sum:

显示或检查 MD5(32-bit) 校验和,若没有文件选项,或者文件处为”-“,则从标准输入读取。
echo -n : 不打印换行符。
cut: cut用来从标准输入或文本文件中剪切列或域。剪切文本可以将之粘贴到一个文本文件。
-d 指定与空格和tab键不同的域分隔符。-f1 表示第一个域。

1
2
3
4
5
md5sum iot_gateway
5b5e4be07a7960ef1450f3e8c8b22df9 iot_gateway

md5sum iot_gateway | cut -d ' ' -f1
5b5e4be07a7960ef1450f3e8c8b22df9

shell删除某个字符

例如使用 tr -d '[ \t] 删除换行字符

1
echo "    123  567   " | tr -d '[ \t]' #输出12345

使用shell添加头部信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/bin/bash

# 删除旧固件
rm -rf ./iot_gateway*.elf
rm -rf ./iot_gateway*.ipk

# 生成的固件路径
FIRMWARE_ELF_DIR= xxx
FIRMWARE_IPK_DIR= xxx

# 固件加头打包时间
PACK_TIME=$(date "+%Y-%m-%d %H:%M:%S")

echo "packtime: $PACK_TIME"

# 拷贝固件至当前目录暂存
cp $FIRMWARE_ELF_DIR/iot_gateway ./raw.elf
cp $FIRMWARE_IPK_DIR/iot_gateway*.ipk ./raw.ipk

echo "copy firmware done."

# 获取固件的大小
ELF_SIZE=$(ls -l ./raw.elf | awk '{print $5}')
IPK_SIZE=$(ls -l ./raw.ipk | awk '{print $5}')

echo "elf size: $ELF_SIZE"
echo "ipk size: $IPK_SIZE"

# 计算固件的MD5
ELF_MD5=$(md5sum raw.elf | cut -d ' ' -f1)
IPK_MD5=$(md5sum raw.ipk | cut -d ' ' -f1)

echo "elf md5: $ELF_MD5"
echo "ipk md5: $ELF_MD5"

# 填充固件头信息到json
cat firmware.json |
jq -c "to_entries |
map(if .key == \"size\"
then . + {\"value\":$ELF_SIZE}
elif .key == \"format\"
then . + {\"value\":\"elf\"}
elif .key == \"timestamp\"
then . + {\"value\":\"$PACK_TIME\"}
elif .key == \"md5\"
then . + {\"value\":\"$ELF_MD5\"}
else .
end
) |
from_entries" | tr -d '\n' > elf.json

cat firmware.json |
jq -c "to_entries |
map(if .key == \"size\"
then . + {\"value\":$IPK_SIZE}
elif .key == \"format\"
then . + {\"value\":\"ipk\"}
elif .key == \"timestamp\"
then . + {\"value\":\"$PACK_TIME\"}
elif .key == \"md5\"
then . + {\"value\":\"$IPK_MD5\"}
else .
end
) |
from_entries" | tr -d '\n' > ipk.json

#Debug: printf json信息
echo "elf.json:"
cat ./elf.json | jq .

echo "ipk.json:"
cat ./ipk.json | jq .

# 获取软件版本
ELF_APP_VER=$(cat ./elf.json | jq --raw-output '.appVer')
IPK_APP_VER=$(cat ./ipk.json | jq --raw-output '.appVer')

echo "elf app version: $ELF_APP_VER"
echo "ipk app version: $IPK_APP_VER"

# 计算json的长度,16进制字符串格式
ELF_JSON_LEN=$(ls -l ./elf.json | awk '{printf "%04x\n",$5}')
IPK_JSON_LEN=$(ls -l ./ipk.json | awk '{printf "%04x\n",$5}')

echo "elf json len: $ELF_JSON_LEN"
echo "ipk json len: $IPK_JSON_LEN"

# 保存到文件
echo -n "$ELF_JSON_LEN" >elf.len
echo -n "$IPK_JSON_LEN" >ipk.len


# 合并文件
cat elf.len elf.json raw.elf >iot_gateway_$ELF_APP_VER.elf
cat ipk.len ipk.json raw.ipk >iot_gateway_$ELF_APP_VER.ipk

echo "Generate Pakage Done."

# 删除生成的临时文件
rm -rf elf.json ipk.json raw.elf raw.ipk elf.len ipk.len