Skip to content

Instantly share code, notes, and snippets.

@geekman
Created October 26, 2015 16:30

Revisions

  1. geekman created this gist Oct 26, 2015.
    148 changes: 148 additions & 0 deletions jffs2.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,148 @@
    #!/usr/bin/env python
    #
    # tool to parse JFFS2 images
    # and more importantly, guess the erase block size
    #
    # 2015.10.19 darell tan
    #

    from struct import unpack
    from argparse import ArgumentParser
    import os
    import sys


    JFFS2_MAGIC = 0x1985

    def detect_endian(f):
    d = f.read(2)
    f.seek(-2, 1)

    if unpack('<H', d)[0] == JFFS2_MAGIC:
    return '<'
    elif unpack('>H', d)[0] == JFFS2_MAGIC:
    return '>'
    return None

    class Node:
    def __init__(self, f, endian):
    self.f = f
    self.pos = f.tell()

    elems = self.parse_node(endian)
    if elems is None:
    raise IOError, 'parse error'

    self.magic, self.nodetype, self.totlen, self.hdr_crc = elems

    assert self.magic == JFFS2_MAGIC or \
    self.magic == self.nodetype == 0xffff, 'invalid JFFS2 magic'

    if self.is_free():
    self.f.seek(-12 + 4, 1)
    while self.f.read(4) == '\xff' * 4:
    pass
    # check for EOF
    if self.f.read(1) != '':
    self.f.seek(-5, 1) # put the bytes back
    self.totlen = self.f.tell() - self.pos
    else:
    self.f.seek(self.pad_len(self.totlen - 12), 1)

    @staticmethod
    def pad_len(x):
    return (x + 3) & ~3

    def parse_node(self, endian):
    d = self.f.read(12)
    if d == '':
    raise EOFError

    # could be just padding. if so extend it.
    if len(d) < 12 and d == '\xff' * len(d):
    d += '\xff' * (12 - len(d))

    return unpack(endian + 'HHII', d)

    def __str__(self):
    return '%08x %04x %04x %d' % (self.pos, self.magic,
    self.nodetype, self.totlen)

    def is_free(self):
    return self.magic == self.nodetype == 0xffff


    def parse_jffs2(f):
    e = detect_endian(f)
    assert e is not None, 'JFFS2 magic not present'

    nodes = []
    try:
    while True:
    nodes.append(Node(f, e))
    except EOFError:
    pass

    return nodes

    def get_block_size(start_addrs):
    diffs = [b - a for a, b in zip(start_addrs[:-1], start_addrs[1:])]
    diffs = sorted(diffs)

    # get the lowest common denominator
    while len(diffs) > 1:
    if diffs[-1] / diffs[0]:
    del diffs[-1]

    return diffs

    def main():
    ap = ArgumentParser()
    ap.add_argument('--pad', '-p', type=int, nargs='?', default=None,
    help='Pads the image file to block size (specify 0 to auto-detect)')
    ap.add_argument('--verbose', '-v', action='store_true', help='Dumps all found nodes')
    ap.add_argument('image_file', help='JFFS image file')
    args = ap.parse_args()

    f = open(args.image_file, 'r+b')
    nodes = parse_jffs2(f)

    if args.verbose:
    print ' offset magic type len' # header
    for n in nodes:
    print n

    # find the start addresses
    start_addrs = []
    prev_free = False
    for n in nodes:
    if not n.is_free() and prev_free:
    start_addrs.append(n.pos)
    prev_free = n.is_free()

    blocksizes = get_block_size(start_addrs)
    for bs in blocksizes:
    print 'block size', bs

    # pad the image?
    if args.pad is not None:
    f.seek(0, 2) # seek to end
    filesize = f.tell()

    assert args.pad > 0 or len(blocksizes) == 1, \
    'ambiguous block size - must specify'

    block_sz = args.pad if args.pad > 0 else blocksizes[0]
    if filesize % block_sz == 0:
    print 'file size %d already conforms to block size %d' % \
    (filesize, block_sz)
    else:
    target_filesize = (1 + (filesize - 1) // block_sz) * block_sz
    print 'padding image to %d (block size %d)...' % (target_filesize, block_sz)

    f.write('\xff' * (target_filesize - filesize))

    f.close()

    if __name__ == '__main__':
    main()
    48 changes: 48 additions & 0 deletions mount-jffs2
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,48 @@
    #!/bin/sh
    #
    # mounts jffs2 images
    #
    # 2015.10.18 darell tan
    #

    set -e

    JFFS=$1
    MOUNTPOINT=$2
    ERASESZ=$3
    DEV=`losetup -f`


    if [ ! -f "$JFFS" ]; then
    echo "JFFS2 image $JFFS not found"
    exit 1
    elif [ -z "$MOUNTPOINT" -o ! -d "$MOUNTPOINT" ]; then
    echo "mountpoint $MOUNTPOINT not specified or doesn't exist"
    exit 1
    elif [ -z "$ERASESZ" ]; then
    echo "specify erase size"
    exit 1
    fi

    rmblock2mtd() {
    if grep ^block2mtd /proc/modules >/dev/null; then
    rmmod block2mtd
    fi
    }

    cleanup() {
    [ -e "$DEV" ] && losetup -d $DEV
    rmblock2mtd 2>/dev/null
    }

    trap cleanup EXIT

    # setup loop device
    losetup $DEV $JFFS

    rmblock2mtd
    modprobe block2mtd block2mtd="$DEV,$ERASESZ"
    modprobe mtdblock || true

    mount -t jffs2 /dev/mtdblock0 $MOUNTPOINT

    530 changes: 530 additions & 0 deletions uImage.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,530 @@
    #!/usr/bin/env python
    ###############################################################################
    ###
    ### (c) 2012 by Ollopa / www.isysop.com
    ###
    ### Python script to pack and unpack U-Boot uImage files
    ###
    ###

    import os, time, zlib
    from struct import *
    from os import fstat
    from optparse import OptionParser, OptionGroup

    ### 64-byte header structure:
    ### uint32 magic_number
    ### uint32 header_crc
    ### uint32 timestamp
    ### uint32 uImage_size
    ### uint32 load_address
    ### uint32 entry_address
    ### uint32 data_crc
    ### uint8 os_type
    ### uint8 architecture
    ### uint8 image_type
    ### uint8 compression_type
    ### uint8 image_name[32]
    HEADER_FORMAT = '!7L4B32s' ### (Big-endian, 7 ULONGS, 4 UCHARs, 32-byte string)
    HEADER_SIZE=calcsize(HEADER_FORMAT) ### Should be 64-bytes

    ### Image types, from 0 to 14
    imageType = [['INVALID', ''],
    ['Standalone', 'standalone'],
    ['Kernel', 'kernel'],
    ['RAMDisk', 'ramdisk'],
    ['Multi-File', 'multi'],
    ['Firmware', 'firmware'],
    ['Script', 'script'],
    ['Filesystem', 'filesystem'],
    ['Flat Device Tree Blob', 'flat_dt'],
    ['Kirkwood Boot', 'kwbimage'],
    ['Freescale IMXBoot', 'imximage'],
    ['Davinci UBL', 'ublimage'],
    ['OMAP Config Header', 'omapimage'],
    ['Davinci AIS', 'aisimage'],
    ['Kernel (any load address)', 'kernel_noload']]

    ### OS types from 0 to 22
    osType = [['INVALID', ''],
    ['OpenBSD', 'openbsd'],
    ['NetBSD', 'netbsd'],
    ['FreeBSD', 'freebsd'],
    ['4.4BSD', '4_4bsd'],
    ['Linux', 'linux'],
    ['SVR4', 'svr4'],
    ['Esix', 'esix'],
    ['Solaris', 'solaris'],
    ['Irix', 'irix'],
    ['SCO', 'sco'],
    ['Dell', 'dell'],
    ['NCR', 'ncr'],
    ['LynxOS', 'lynxos'],
    ['VxWorks', 'vxworks'],
    ['pSOS', 'psos'],
    ['QNX', 'qnx'],
    ['U-Boot Firmware', 'u-boot'],
    ['RTEMS', 'rtems'],
    ['Unity OS', 'unity'],
    ['INTEGRITY', 'integrity'],
    ['OSE', 'ose']]

    ### CPU Architecture types from 0 to 21
    archType = [['INVALID', ''],
    ['Alpha', 'alpha'],
    ['ARM', 'arm'],
    ['Intel x86', 'x86'],
    ['IA64', 'ia64'],
    ['MIPS', 'mips'],
    ['MIPS64', 'mips64'],
    ['PowerPC', 'ppc'],
    ['IBM S390', 's390'],
    ['SuperH', 'sh'],
    ['Sparc', 'sparc'],
    ['Sparc64', 'sparc64'],
    ['M68k', 'm68k'],
    ['MicroBlaze', 'microblaze'],
    ['Nios-II', 'nios2'],
    ['Blackfin', 'blackfin'],
    ['AVR32', 'avr32'],
    ['ST200', 'st200'],
    ['NDS32', 'nds32'],
    ['OpenRISC 1000', 'or1k']]

    ### Compression types from 0 to 4
    compressType = [['uncompressed', 'none'],
    ['gzip compressed', 'gzip'],
    ['bzip2 compressed', 'bzip2'],
    ['lzma compressed', 'lzma'],
    ['lzo compressed', 'lzo']]



    ################################################################################
    ###
    ### fromTable(table, index) - Returns string from lookup table
    ###
    ### parameters: table: name of table to search
    ### index: index of string to fetch
    ###
    ### Returns: Name fromt table or "unknown" message
    ###
    def fromTable(table, index):
    if index < len(table):
    string = table[index][0]
    else:
    string = "Unknown:" + str(index)
    return string

    ################################################################################
    ###
    ### searchTable(table, value) - Searches a table for value, returns index
    ###
    ### parameters: table: name of table to search
    ### value: data to search for in table
    ###
    ### Returns: index of value if found, -1 otherwise
    ###
    def searchTable(table, value):
    index = -1
    for i in range(len(table)):
    if table[i][1] == value:
    index = i
    break
    return index

    ################################################################################
    ###
    ### parseHeader(fh, offset=0) - Parse uImage header located at offset in file
    ###
    ### Parameters: fh: file handle
    ### offset: Optional location of header within file
    ###
    ### Returns: Dictionary of header information
    ###
    def parseHeader(fh, offset=0):
    ### Save current position and seek to start position
    startpos = fh.tell()
    fh.seek(offset)

    try:
    block = fh.read(HEADER_SIZE)
    except IOError:
    print "File read error"
    exit(1)

    ### Names of fields in the image header
    keys = ['magic', 'headerCrc', 'time', 'size', 'loadAddr', 'entryAddr',
    'dataCrc', 'osType', 'arch', 'imageType', 'compression', 'name']

    ### Unpack the header into a dictionary of (key,value) pairs
    values = unpack(HEADER_FORMAT, block)
    hd = dict(zip(keys, values))

    ### if Multi-file image, append file information
    if hd['imageType'] == 4:
    hd['files'] = getMultiFileLengths(fh, fh.tell())
    ### Restore saved file position
    fh.seek(startpos)
    return hd

    ################################################################################
    ###
    ### calculateHeaderCrc(hd) - Calculates the crc for a header
    ###
    ### Parameters: hd: Dictionary of header
    ###
    ### Returns: crc32 value
    ###
    def calculateHeaderCrc(hd):
    ### Re-pack the list into a binary string
    ### Must calclate header CRC with CRC field set to 0.
    header = pack(HEADER_FORMAT, hd['magic'], 0, hd['time'], hd['size'],
    hd['loadAddr'], hd['entryAddr'], hd['dataCrc'], hd['osType'],
    hd['arch'], hd['imageType'], hd['compression'], hd['name'])
    return (zlib.crc32(header) & 0xffffffff)

    ################################################################################
    ###
    ### crc32File(fh, start, length) - Calculates crc of data segment in a file
    ###
    ### Parameters: fh: file handle
    ### start: start position (optional)
    ### length: length of segment (optional)
    ###
    def crc32File(fh, start=0, length=float('inf')):
    BLOCKSIZE = 1024*512
    crc32 = 0
    byteCount = 0
    ### Save current position and seek to start position
    startpos = fh.tell()
    fh.seek(start)

    block = fh.read(BLOCKSIZE)
    while block != "":
    byteCount += len(block)
    if byteCount > length:
    extra = byteCount - length
    block = block[:-extra]
    crc32 = zlib.crc32(block, crc32)
    if byteCount >= length:
    break;
    block = fh.read(BLOCKSIZE)

    ### Restore saved file position
    fh.seek(startpos)
    return (crc32 & 0xffffffff)

    ################################################################################
    ###
    ### getMultiFileLengths(fh) - returns list of file lengths in multi-file image
    ###
    ### Parameters: fh: file handle
    ### offset: location of file lengths
    ###
    def getMultiFileLengths(fh, offset=HEADER_SIZE):
    lengthList = []
    ### Save current position and seek to multi-file table location
    startpos = fh.tell()
    fh.seek(offset)

    block = fh.read(4)
    while block != "":
    length = unpack('!L', block)[0]
    if length == 0:
    break
    lengthList.append(length)
    block = fh.read(4)

    ### Restore saved file position
    fh.seek(startpos)
    return lengthList

    ################################################################################
    ###
    ### dumpHeader(hd) - Dumps header in text form to stdout
    ###
    ### Parameters: hd: header dictionary
    def dumpHeader(hd):
    ### Dump header information and verify CRCs
    if hd['magic'] != 0x27051956:
    print "Invalid magic number! This is not a valid uImage file."
    print "Magic: expected 0x27051956, but found %#08x" % hd['magic']
    return
    print "Image name:\t",
    print hd['name'].rstrip('\o')
    print "Created:\t", time.ctime(hd['time'])
    print "Image type:\t",
    print fromTable(archType, hd['arch']),
    print fromTable(osType, hd['osType']),
    print fromTable(imageType, hd['imageType']),
    print "(" + fromTable(compressType, hd['compression']) + ")"
    print "Data size:\t%u Bytes" % hd['size']
    print "Load Address:\t%#08x" % hd['loadAddr']
    print "Entry Point:\t%#08x" % hd['entryAddr']


    print "Header CRC:\t%#08x ..." % hd['headerCrc'],
    if hd['headerCrc'] == calculateHeaderCrc(hd):
    print "OK"
    else:
    print "Mismatch! Calculated CRC: %#08x" % str(calculatedHeadedCrc(hd))

    print "Data CRC:\t%#08x" % hd['dataCrc'] ###,


    ###### Verify Data CRC
    ###print "%#08x" % crc32File(fh)

    if hd['imageType'] == 4:
    print "Contents:"
    for index, length in enumerate(hd['files']):
    print " Image %u: %u bytes" % (index,length)
    return

    ################################################################################
    ###
    ### dumpToFile(fh, offset, len, filename) - Dumps segment of fh to filename
    ###
    ### Parameters: fh: Source file handle
    ### offset: Start position in source file
    ### length: Length of segment to copy
    ### filename: Name of new file to write contents to
    ###
    def dumpToFile(fh, offset, length, filename):
    ### Save current position and seek to start location
    startpos = fh.tell()
    fh.seek(offset)
    BLOCKSIZE = 1024*512
    df = open(filename, "wb")
    while True:
    if BLOCKSIZE > length:
    BLOCKSIZE = length
    block = fh.read(BLOCKSIZE)
    df.write(block)
    length -= BLOCKSIZE
    if block == "":
    break;

    df.close()
    ### Restore saved file position
    fh.seek(startpos)
    return

    ################################################################################
    ###
    ### imageList(image) - lists contents of image to stdout
    ###
    ### Parameters: image: filename of image to list
    ###
    def imageList(image):
    try:
    size = os.path.getsize(image)
    except IOError:
    print "inavlid filename:", image
    exit(1)

    if size < HEADER_SIZE:
    print "File too small! Not a uImage file."
    exit(1)

    f = open(image, "rb")
    try:
    data = f.read(HEADER_SIZE)
    except IOError:
    print "File read error"
    f.close()
    exit(1)


    d=parseHeader(f)

    dumpHeader(d)

    ### Dump multi-file headers as well
    if d['imageType'] == 4:
    ### Next image begins after multi-file table
    f.seek(HEADER_SIZE+4+(4*len(d['files'])))
    for index, length in enumerate(d['files']):
    print
    print "Multi-File Image %u: Header" % (index)
    print "--------------------------"
    mfd = parseHeader(f, f.tell())
    dumpHeader(mfd)
    ### Dump child uImage file
    #dumpToFile(f, f.tell(), length, mfd['name'].rstrip('\0')+".uImage")
    ### Dump payload from child uImage
    #dumpToFile(f, f.tell()+HEADER_SIZE, mfd['size'], mfd['name'].rstrip('\0'))
    f.seek(length,1)
    f.close()


    ################################################################################
    ###
    ### imageExtract(image) - extracts contents of image to file(s)
    ###
    ### Parameters: image: filename of image to extract
    ###
    def imageExtract(image):

    try:
    size = os.path.getsize(image)
    except IOError:
    print "inavlid filename", image
    exit(1)

    if size < HEADER_SIZE:
    print "File too small! Not a uImage file."
    exit(1)

    f = open(image, "rb")
    try:
    data = f.read(HEADER_SIZE)
    except IOError:
    print "File read error"
    f.close()
    exit(1)


    d=parseHeader(f)

    ### Check for multi-file image
    if d['imageType'] == 4:
    ### Next image begins after multi-file table
    f.seek(HEADER_SIZE+4+(4*len(d['files'])))
    filenames = []
    for index, length in enumerate(d['files']):
    mfd = parseHeader(f, f.tell())
    filename = mfd['name'].rstrip('\0')
    if filename == "":
    filename = "image"+index
    ### Prevent overwriting files with the same name
    if filename in filenames:
    suffix = 1
    while filename+"_"+str(suffix) in filenames:
    suffix += 1
    filename = filename + "_" + str(suffix)
    filenames.append(filename)
    ### Dump child uImage file
    print filename+".uImage"
    dumpToFile(f, f.tell(), length, filename+".uImage")
    ### Dump payload from child uImage
    print filename
    dumpToFile(f, f.tell()+HEADER_SIZE, mfd['size'], filename)
    f.seek(length,1)
    else:
    filename = d['name'].rstrip('\0')
    if filename == "":
    filename = "image0"
    print d['name'].rstrip('\0')
    dumpToFile(f, HEADER_SIZE, d['size'], filename)
    f.close()

    ################################################################################
    ###
    ### imageCreate(options, image) - creates uImage with provided options
    ###
    ### Parameters: image: filename of image to create
    ### options: uImage header options from command-line
    ###
    def imageCreate(options, image):
    ifh = open(image, 'w+b')
    os = searchTable(osType, options.osType)
    arch = searchTable(archType, options.architecture)
    it = searchTable(imageType, options.imageType)
    le = int(options.loadaddr, 0)
    ep = int(options.entryaddr, 0)
    comp = searchTable(compressType, options.compression)
    dcrc = 0
    hcrc = 0
    size = 0
    data = str()
    ### unix timestamp (or set to 0 to be like Palm)
    ctime = int(time.time())

    ### Check if multi-image
    if it == 4:
    lenTable = []
    for filename in options.filespec.split(":"):
    dfh = open(filename, 'rb')
    lenTable.append(fstat(dfh.fileno()).st_size)
    data += dfh.read()
    dfh.close()
    lenTable.append(0)
    fmt = "!" + str(len(lenTable)) + "L"
    data = pack(fmt, *lenTable) + data
    else:
    dfh = open(options.filespec, 'rb')
    data = dfh.read()
    dfh.close()
    size = len(data)
    dcrc = zlib.crc32(data) & 0xFFFFFFFF
    header = pack(HEADER_FORMAT, 0x27051956, 0, ctime, size,
    le, ep, dcrc, os, arch, it, comp, options.imagename)
    hcrc = zlib.crc32(header) & 0xFFFFFFFF
    header = pack(HEADER_FORMAT, 0x27051956, hcrc, ctime, size,
    le, ep, dcrc, os, arch, it, comp, options.imagename)
    ifh.write(header)
    ifh.write(data)
    ifh.close()
    imageList(image)

    ################################################################################
    ###
    ### Main program body
    ###
    def main():
    usage = "\n\t%prog -l image" \
    "\n\t%prog -c [options] image" \
    "\n\t%prog -x image" \
    "\n\t%prog -h"
    parser = OptionParser(usage)
    parser.add_option("-l", action = "store_true", dest = "imageList",
    help = "list image contents")
    parser.add_option("-c", action = "store_true", dest = "imageCreate",
    help = "create new image")
    parser.add_option("-x", action = "store_true", dest = "imageExtract",
    help="extract image contents")

    group = OptionGroup(parser, "Creation Options", "-c -A arch -O os -T" \
    " type -C comp -a addr -e ep -n name -d data_file[:data_file...] image")
    group.add_option("-A", dest = "architecture", help = "set architecture to 'arch'")
    group.add_option("-O", dest = "osType", help = "set operating system to 'os'")
    group.add_option("-T", dest = "imageType", help = "set image type to 'type'")
    group.add_option("-C", dest = "compression", help = "set compression to 'comp'")
    group.add_option("-a", dest = "loadaddr", help = "iset load address to 'addr'")
    group.add_option("-e", dest = "entryaddr", help = "iset entry point to 'ep'")
    group.add_option("-n", dest = "imagename", help = "set image name to 'name'")
    group.add_option("-d", dest = "filespec", help = "image data from 'datafile'")
    parser.set_defaults(osType="linux", loadaddr="0", entryaddr="0", imagename="",
    compression="none", architecture="x86")
    parser.add_option_group(group)

    (options, args) = parser.parse_args()

    if len(args) != 1:
    parser.error("incorrect number of arguments")

    image = args[0]

    if options.imageList:
    if options.imageCreate or options.imageExtract:
    parser.error("-l, -c, and -x are mutually exclusive")
    imageList(image)
    elif options.imageExtract:
    if options.imageList or options.imageCreate:
    parser.error("-l, -c, and -x are mutually exclusive")
    imageExtract(image)
    elif options.imageCreate:
    if options.imageList or options.imageExtract:
    parser.error("-l, -c, and -x are mutually exclusive")
    if not options.imageType:
    parser.error("Must specify image type")
    if not options.filespec:
    parser.error("Must specify data file")
    imageCreate(options, image)

    return

    if __name__ == "__main__":
    main()