├── .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 | --------------------------------------------------------------------------------