├── .gitignore
├── LICENSE
├── README.md
├── createnpk.py
├── dumpnpk.py
└── unpacknpk.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | __pycache__
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kost/mikrotik-npk/d54e97caac9ea447e29939ca4176d17eeff856a9/LICENSE
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | mikrotik-npk
2 | ============
3 |
4 | Python tools for manipulating Mikrotik NPK format
5 |
6 | Source
7 | ======
8 | Original scripts were found on:
9 | http://routing.explode.gr/node/96
10 |
11 |
--------------------------------------------------------------------------------
/createnpk.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | DESC_SHORT = 'routing'
4 | DESC_LONG = '\n Quagga 0.98.6-5\n '
5 | VER = '\x1bf\t\x02' # 2.9.27
6 |
7 | ONINSTALL = '\n new-libs\n update-console\n '
8 | ONUNINSTALL = '\n dead-libs\n update-console\n '
9 |
10 | import sys
11 | import zlib
12 | import os
13 | import os.path
14 | import stat
15 |
16 | from struct import pack, unpack
17 | from time import time
18 |
19 | #BUILD = pack("I", int(time()))
20 | BUILD = '\xf5\xf7\xa8D'
21 |
22 | def create_part(type, data):
23 |
24 | if type == 4:
25 | data = zlib.compress(data)
26 | dsize = len(data)
27 |
28 | res = ""
29 | res += pack("H", type)
30 | res += pack("I", dsize)
31 | res += data
32 |
33 | return res
34 |
35 | def get_contents(directory):
36 | if not os.path.isdir(directory):
37 | return
38 | res = []
39 | for i in os.listdir(directory):
40 | ii = os.path.join(directory, i)
41 | res.append(i)
42 | if os.path.isdir(ii) and not os.path.islink(ii):
43 | for j in get_contents(ii):
44 | res.append(os.path.join(i, j))
45 | return res
46 |
47 | def create_data(directory):
48 | res = ""
49 | print directory
50 | contents = get_contents(directory)
51 | contents.sort()
52 | for i in contents:
53 | ii = os.path.join(directory, i)
54 |
55 | dsize = 0
56 | if os.path.isdir(ii):
57 | data = ""
58 | mode = os.stat(ii)[stat.ST_MODE]
59 | modestr = pack("H", mode)
60 | rtype = 65
61 | perm = 237
62 | elif os.path.islink(ii):
63 | data = os.readlink(ii)
64 | dsize = len(data)
65 | # type=161(A1), perm=255(FF)
66 | rtype = 161
67 | perm = 255
68 | modestr = '\xFF\xA1'
69 | else:
70 | f = open(ii, "r")
71 | data = f.read()
72 | f.close()
73 | dsize = len(data)
74 | mode = os.stat(ii)[stat.ST_MODE]
75 | rtype = 129
76 | if mode & stat.S_IXUSR:
77 | perm = 237
78 | else:
79 | perm = 164
80 |
81 | modestr = pack("BB", perm, rtype)
82 |
83 | try:
84 | tim = os.stat(ii)[stat.ST_MTIME]
85 | except OSError:
86 | tim = 0
87 |
88 | header = modestr + '\x00\x00'+ '\x00\x00\x00\x00' + pack("I", tim)
89 | header += VER + BUILD + '\x00\x00\x00\x00'
90 | header += pack("I", dsize) + pack("H", len(i))
91 |
92 | res += header + i + data
93 | return res
94 |
95 | # Read files
96 |
97 | if len(sys.argv) != 2:
98 | print "Usage: %s
" % (sys.argv[0])
99 | sys.exit(2)
100 |
101 | data = create_data(sys.argv[1])
102 |
103 | # Create parts
104 |
105 | parts = ""
106 | parts += create_part(7, ONINSTALL) # Oninstall
107 | parts += create_part(8, ONUNINSTALL) # Onuninstall
108 | parts += create_part(4, data) # Data
109 |
110 | # Create header
111 |
112 | header = ""
113 | header += '\x1e\xf1\xd0\xba'
114 | header += '\x00\x00\x00\x00' # Size... fill it in later
115 | header += '\x01\x00 \x00\x00\x00'
116 | shortd = DESC_SHORT
117 | while len(shortd) < 16:
118 | shortd += '\x00'
119 | header += shortd
120 | header += VER
121 | header += BUILD
122 | header += '\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x04\x00\x00\x00i386\x02\x00' # Unknown stuff
123 | header += pack("I", len(DESC_LONG))
124 | header += DESC_LONG
125 | header += '\x03\x00"\x00\x00\x00\x01\x00system\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
126 | header += VER + '\x00\x00\x00\x00'
127 | header += VER + '\x00\x00\x00\x00'
128 |
129 | header = header[0:4] + pack("I", len(header) + len(parts) - 8) + header[8:]
130 |
131 | f = open(sys.argv[1] + ".npk", "w")
132 | f.write(header)
133 | f.write(parts)
134 | f.close()
135 |
--------------------------------------------------------------------------------
/dumpnpk.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # npk format
4 | # ---
5 | # 0-4 : '\x1e\xf1\xd0\xba'
6 | # 4-8 : len(data - 8) ===> The size of the package
7 | # 8-14 : '\x01\x00 \x00\x00\x00'
8 | # 14-30: description ===> 16 chars to put a short name
9 | # 30-34: ?? | ==> version #1 - used in this header again (revision, 'f' (102), minor, major)
10 | # 34-38: ?? | ==> version #2 - used in the data part (epoch time of package build)
11 | # | Actualy seems like header[30:42] == each_data_header[12:24]...
12 | # | Both appear as integers in /var/pdb/.../version
13 | # 38-42: 0
14 | # 42-46: 0
15 | # 46-48: 16
16 | # 48-50: 4 |
17 | # 50-52: 0 | ==> Maybe int size of the architecture identifier that follows
18 | # 52-56: "i386"
19 | # 56-58: 2
20 | # 58-62: long description size ===> how many chars follow
21 | # 62-x : long description text
22 | # +24: '\x03\x00"\x00\x00\x00\x01\x00system\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
23 | # +8 : '2f\t\x02\x00\x00\x00\x00' |
24 | # +8 : '2f\t\x02\x00\x00\x00\x00' | ==> two same headers (separators?)
25 | # first 4 like header[30:34]
26 | # last 4 always 0
27 | #
28 | # what follows are headers of 1 short (type) and 1 int (size):
29 | # type 7, 8: some kind of script
30 | # type 4: data
31 | #
32 | # the content is directly after the header
33 | #
34 | # in case of data it is commpressed with zlib
35 | #
36 | # uncompressed data has 30 byte headers:
37 | #
38 | # 0-1 : permissions: 237 is executable (755), 164 is non executable (644) |
39 | # 1-2 : file type: 65 dir, 129 file | ==> ST_MODE from stat()
40 | # 2-4 : 0 - could be user/group
41 | # 4-8 : 0 - could be user/group
42 | # 8-12 : last modification time (ST_MTIME) as reported by stat()
43 | # 12-24: version stuff and a 0 (see above...)
44 | # 24-28: data size
45 | # 28-30: file name size
46 | #
47 | # then comes the file name and after that the data
48 |
49 | import sys
50 | import zlib
51 |
52 | from struct import pack, unpack
53 | from time import ctime
54 |
55 | def parse_npk(filename):
56 |
57 | f = open(filename, "r")
58 | data = f.read()
59 | f.close()
60 |
61 | header = data[:62]
62 | dsize = unpack("I", header[58:62])[0] # Description size
63 | header += data[62:62+dsize+40]
64 |
65 | print repr(header[38:58])
66 | print "Magic:", repr(header[0:4]), "should be:", repr('\x1e\xf1\xd0\xba')
67 | print "Size after this:", unpack("I", header[4:8])[0], "Header size:", len(header), "Data size:", len(data)
68 | print "Unknown stuff:", repr(header[8:14]), "should be:", repr('\x01\x00 \x00\x00\x00')
69 | print "Short description:", header[14:30]
70 | print "Revision, unknown, Minor, Major:", repr(header[30:34]), unpack("BBBB", header[30:34])
71 | print "Build time:", repr(header[34:38]), ctime(unpack("I", header[34:38])[0])
72 | print "Some other numbers:", unpack("IIHHH", header[38:52]), "should be: (0, 0, 16, 4, 0)"
73 | print "Architecture:", header[52:56]
74 | print "Another number:", unpack("H", header[56:58]), "should be: (2,)"
75 | print "Long description:", repr(header[62:62+dsize])
76 | print "Next 24 chars:", repr(header[62+dsize:62+dsize+24])
77 | print " should be:", repr('\x03\x00"\x00\x00\x00\x01\x00system\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
78 | print "Separators:", repr(header[62+dsize+24:62+dsize+32]), repr(header[62+dsize+32:62+dsize+40])
79 | print " first 4:", unpack("BBBB",header[62+dsize+24:62+dsize+28]), unpack("BBBB",header[62+dsize+32:62+dsize+36])
80 | print ""
81 |
82 | data = data[62+dsize:]
83 | res=[]
84 | while len(data)>6:
85 | type = unpack("H", data[:2])[0]
86 | size = unpack("I", data[2:6])[0]
87 | print "Found data of type:", type, "size:", size
88 | contents = data[6:6+size]
89 | if type == 4:
90 | contents = zlib.decompress(contents)
91 | print " Uncompressing data..."
92 | if type == 7:
93 | print " Contents (oninstall):", repr(contents)
94 | if type == 8:
95 | print " Contents (onuninstall):", repr(contents)
96 | res.append({"type": type, "size": size, "contents": contents})
97 | data = data[6+size:]
98 | print ""
99 |
100 | print "Returning the raw header and the rest of the file (each part in a list)"
101 | print ""
102 |
103 | return header, res
104 |
105 | def parse_data(data):
106 | res = []
107 | while len(data)>30:
108 | header = data[:30]
109 | dsize = unpack("I", header[24:28])[0]
110 | fsize = unpack("H", header[28:30])[0]
111 | if len(data) - 30 - fsize - dsize < 0:
112 | dsize = len(data) - 30 - fsize
113 | fstuff = data[30:30+fsize]
114 | dstuff = data[30+fsize:30+fsize+dsize]
115 | res.append({"header": header, "file": fstuff, "data": dstuff})
116 | data = data[30+fsize+dsize:]
117 | return res
118 |
119 | if __name__ == "__main__":
120 | if len(sys.argv) > 1:
121 | for i in sys.argv[1:]:
122 | header, res = parse_npk(i)
123 | for j in res:
124 | if j["type"] == 4:
125 | print "Files in package:"
126 | data = parse_data(j["contents"])
127 | for k in data:
128 | add = ''
129 | perm, type, z1, z2, tim = unpack("BBHII", k["header"][:12])
130 | if perm == 255:
131 | perm = "al"
132 | if perm == 237:
133 | perm = "ex"
134 | if perm == 164:
135 | perm = "nx"
136 | if type == 65:
137 | type = "dir"
138 | if type == 129:
139 | type = "fil"
140 | if type == 161:
141 | type = "sym"
142 | add='=> '+k["data"]
143 | print type, perm, k["file"], add, tim
144 |
--------------------------------------------------------------------------------
/unpacknpk.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # npk format
4 | # ---
5 | # 0-4 : '\x1e\xf1\xd0\xba'
6 | # 4-8 : len(data - 8) ===> The size of the package
7 | # 8-14 : '\x01\x00 \x00\x00\x00'
8 | # 14-30: description ===> 16 chars to put a short name
9 | # 30-34: ?? | ==> version #1 - used in this header again (revision, 'f' (102), minor, major)
10 | # 34-38: ?? | ==> version #2 - used in the data part (epoch time of package build)
11 | # | Actualy seems like header[30:42] == each_data_header[12:24]...
12 | # | Both appear as integers in /var/pdb/.../version
13 | # 38-42: 0
14 | # 42-46: 0
15 | # 46-48: 16
16 | # 48-50: 4 |
17 | # 50-52: 0 | ==> Maybe int size of the architecture identifier that follows
18 | # 52-56: "i386"
19 | # 56-58: 2
20 | # 58-62: long description size ===> how many chars follow
21 | # 62-x : long description text
22 | # +24: '\x03\x00"\x00\x00\x00\x01\x00system\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
23 | # +8 : '2f\t\x02\x00\x00\x00\x00' |
24 | # +8 : '2f\t\x02\x00\x00\x00\x00' | ==> two same headers (separators?)
25 | # first 4 like header[30:34]
26 | # last 4 always 0
27 | #
28 | # what follows are headers of 1 short (type) and 1 int (size):
29 | # type 7, 8: some kind of script
30 | # type 4: data
31 | #
32 | # the content is directly after the header
33 | #
34 | # in case of data it is commpressed with zlib
35 | #
36 | # uncompressed data has 30 byte headers:
37 | #
38 | # 0-1 : permissions: 237 is executable (755), 164 is non executable (644) |
39 | # 1-2 : file type: 65 dir, 129 file | ==> ST_MODE from stat()
40 | # 2-4 : 0 - could be user/group
41 | # 4-8 : 0 - could be user/group
42 | # 8-12 : last modification time (ST_MTIME) as reported by stat()
43 | # 12-24: version stuff and a 0 (see above...)
44 | # 24-28: data size
45 | # 28-30: file name size
46 | #
47 | # then comes the file name and after that the data
48 |
49 | import sys
50 | import zlib
51 | import os
52 |
53 | from struct import pack, unpack
54 | from time import ctime
55 |
56 | def parse_npk(filename):
57 |
58 | f = open(filename, "r")
59 | data = f.read()
60 | f.close()
61 |
62 | if data[10] == "$":
63 | # Switch to newer npk format
64 | print "Version 6 npk reader"
65 |
66 | header = data[:66]
67 | dsize = unpack("I", header[62:66])[0] # Description size
68 | header += data[66:66+dsize+40]
69 |
70 |
71 | print repr(header[38:58])
72 | print "Magic:", repr(header[0:4]), "should be:", repr('\x1e\xf1\xd0\xba')
73 | print "Size after this:", unpack("I", header[4:8])[0], "Header size:", len(header), "Data size:", len(data)
74 | print "Unknown stuff:", repr(header[8:14]), "should be:", repr('\x01\x00 \x00\x00\x00')
75 | print "Short description:", header[14:30]
76 | print "Revision, unknown, Minor, Major:", repr(header[30:34]), unpack("BBBB", header[30:34])
77 | print "Build time:", repr(header[34:38]), ctime(unpack("I", header[34:38])[0])
78 | print "Another number:", repr(header[38:42])
79 | print "Some other numbers:", unpack("IIHHH", header[42:56]), "should be: (0, 2, 16, 4, 0)"
80 | print "Architecture:", header[56:60]
81 | print "Another number:", unpack("H", header[60:62]), "should be: (2,)"
82 | print "Long description:", repr(header[66:66+dsize])
83 | # print "Next 24 chars:", repr(header[62+dsize:62+dsize+24])
84 | # print " should be:", repr('\x03\x00"\x00\x00\x00\x01\x00system\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
85 | # print "Separators:", repr(header[62+dsize+24:62+dsize+32]), repr(header[62+dsize+32:62+dsize+40])
86 | # print " first 4:", unpack("BBBB",header[62+dsize+24:62+dsize+28]), unpack("BBBB",header[62+dsize+32:62+dsize+36])
87 | # print ""
88 |
89 | data = data[66+dsize:]
90 | else:
91 | # Older npk format
92 | print "Version 5 npk reader"
93 | header = data[:62]
94 | dsize = unpack("I", header[58:62])[0] # Description size
95 | header += data[62:62+dsize+40]
96 |
97 | print repr(header[38:58])
98 | print "Magic:", repr(header[0:4]), "should be:", repr('\x1e\xf1\xd0\xba')
99 | print "Size after this:", unpack("I", header[4:8])[0], "Header size:", len(header), "Data size:", len(data)
100 | print "Unknown stuff:", repr(header[8:14]), "should be:", repr('\x01\x00 \x00\x00\x00')
101 | print "Short description:", header[14:30]
102 | print "Revision, unknown, Minor, Major:", repr(header[30:34]), unpack("BBBB", header[30:34])
103 | print "Build time:", repr(header[34:38]), ctime(unpack("I", header[34:38])[0])
104 | print "Some other numbers:", unpack("IIHHH", header[38:52]), "should be: (0, 0, 16, 4, 0)"
105 | print "Architecture:", header[52:56]
106 | print "Another number:", unpack("H", header[56:58]), "should be: (2,)"
107 | print "Long description:", repr(header[62:62+dsize])
108 | # print "Next 24 chars:", repr(header[62+dsize:62+dsize+24])
109 | # print " should be:", repr('\x03\x00"\x00\x00\x00\x01\x00system\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
110 | # print "Separators:", repr(header[62+dsize+24:62+dsize+32]), repr(header[62+dsize+32:62+dsize+40])
111 | # print " first 4:", unpack("BBBB",header[62+dsize+24:62+dsize+28]), unpack("BBBB",header[62+dsize+32:62+dsize+36])
112 | # print ""
113 |
114 | res=[]
115 | while len(data)>6:
116 | type = unpack("H", data[:2])[0]
117 | size = unpack("I", data[2:6])[0]
118 | print "Found data of type:", type, "size:", size
119 | contents = data[6:6+size]
120 | #print contents
121 | if type == 3:
122 | print " Contents (system):", repr(contents)
123 | if type == 4:
124 | contents = zlib.decompress(contents)
125 | print " Uncompressing data..."
126 | if type == 7:
127 | print " Contents (oninstall):", repr(contents)
128 | if type == 8:
129 | print " Contents (onuninstall):", repr(contents)
130 | if type == 21:
131 | print " Squash File System"
132 | res.append({"type": type, "size": size, "contents": contents})
133 | data = data[6+size:]
134 | print ""
135 |
136 | print "Returning the raw header and the rest of the file (each part in a list)"
137 | print ""
138 |
139 | return header, res
140 |
141 | def parse_data(data):
142 | res = []
143 | while len(data)>30:
144 | header = data[:30]
145 | dsize = unpack("I", header[24:28])[0]
146 | fsize = unpack("H", header[28:30])[0]
147 | if len(data) - 30 - fsize - dsize < 0:
148 | dsize = len(data) - 30 - fsize
149 | fstuff = data[30:30+fsize]
150 | dstuff = data[30+fsize:30+fsize+dsize]
151 | res.append({"header": header, "file": fstuff, "data": dstuff})
152 | data = data[30+fsize+dsize:]
153 | return res
154 |
155 | if __name__ == "__main__":
156 | if len(sys.argv) > 1:
157 | for i in sys.argv[1:]:
158 | header, res = parse_npk(i)
159 | for j in res:
160 | if j["type"] == 21:
161 | print "SquashFS found in package, extract 'squashfs' by using unsquashfs."
162 | f = open("squashfs", "w")
163 | f.write(j["contents"])
164 | f.close()
165 | if j["type"] == 4:
166 | print "Files in package:"
167 | data = parse_data(j["contents"])
168 | for k in data:
169 | perm, type, z1, z2, tim = unpack("BBHII", k["header"][:12])
170 | if type == 65:
171 | type = "dir"
172 | if not os.access(k["file"], os.F_OK):
173 | os.makedirs(k["file"])
174 | if type == 129:
175 | type = "fil"
176 | f = open(k["file"], "w")
177 | f.write(k["data"])
178 | f.close()
179 | os.chmod(k["file"], int(perm))
180 | if type == 161:
181 | type = "sym"
182 | os.symlink(k["data"],k["file"])
183 | # os.chmod(k["file"], int(perm))
184 | if perm == 164:
185 | perm = "nx"
186 | if perm == 237:
187 | perm = "ex"
188 | print type, perm, k["file"], tim
189 |
190 |
--------------------------------------------------------------------------------