#!/usr/bin/env python
"""
LIF Creator - LEGO Digital Designer LIF Creator.
Copyright (C) 2020 sttng
You accept full responsibility for how you use this program.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import os
import sys
import struct
import time
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf-8')
class LIFHeader:
'''
LIF Header (18 bytes total):
4 bytes Char[4] Header (ASCI = 'LIFF')
4 bytes Int32 Spacing (Always equals 0)
4 bytes Int32 Total file size (Int32 big endian)
2 bytes Int16 Value "1" (Int16 big endian)
4 bytes Int32 Spacing (Always equals 0)
'''
def __init__(self):
self.magic = b'LIFF' #Magic Word (ASCI = 'LIFF')
self.spacing1 = struct.pack('>I', 0) #Spacing (Always equals 0)
self.size = struct.pack('>I',3405691582)#0xCAFEBABE - Just to test and also to verify that header size is set correct later
self.spacing2 = struct.pack('>H', 1) #Value "1" (Int16 big endian)
self.spacing3 = struct.pack('>I', 0) #Spacing (Always equals 0)
def setSize(self, size):
self.size = struct.pack('>I', size)
def getSize(self):
return struct.unpack('>I', self.size)[0]
def string(self):
out = b''.join([self.magic, self.spacing1, self.size, self.spacing2, self.spacing3])
return out
class LIFBlock:
'''
LIF Block (20 bytes + data):
2 bytes Int16 Block start/header (always 1)
2 bytes Int16 Block type (1 to 5)
4 bytes Spacing1 (Always equals 0)
4 bytes Int32 Block size in bytes (includes header and data)
4 bytes Spacing2 (Equals 1 for block types 2,4 and 5)
4 bytes Spacing3 (Always equals 0)
X bytes The block content/data.
The block type 1 is the "root block" and its size includes the remainder of the LIF file.
The block type 2 contains the files content/data. The block content seems hard-coded and it is always 1 (Int16) and 0 (Int32).
The block type 3 represents a folder. The block content is a hierarchy of type 3 and 4 blocks.
The block type 4 represents a file. The block data is the file content/data.
The block type 5 contains the files and folders names and some more information.
The block content is a hierarchy of LIF entries.
Note: The block header is 20 bytes total. The data size is equal to the specified size - 20 bytes.
'''
def __init__(self, blocktype, data):
self.blockheader = struct.pack('>H', 1) #Block start/header (always 1)
self.blocktype = struct.pack('>H', blocktype) #Block type (1 to 5)
self.spacing1 = struct.pack('>I', 0) #Spacing (Always equals 0)
#Block size in bytes (includes header and data) <- calculated later
if blocktype == 1 or blocktype == 3:
self.spacing2 = struct.pack('>I', 0)
else:
self.spacing2 = struct.pack('>I', 1) #Spacing (Equals 1 for block types 2,4 and 5)
self.spacing3 = struct.pack('>I', 0) #Spacing (Always equals 0)
if blocktype == 2:
self.data = b'\x00\x01\x00\x00\x00\x00' #The block content seems hard-coded for blocktype 2 and is always 1 (Int16) and 0 (Int32)
else:
self.data = data
self.size = struct.pack('>I',len(self.data)+20) #Block size in bytes (includes header and data)
def setSize(self, size):
self.size = struct.pack('>I', size)
def getSize(self):
return struct.unpack('>I', self.size)[0]
def string(self):
out = b''.join([self.blockheader, self.blocktype, self.spacing1, self.size, self.spacing2, self.spacing3, self.data])
return out
class LIFDirEntry:
'''
SIZE TYPE DESCRIPTION
2 bytes Int16 Entry type (equals 1)
4 bytes Int32 Unknown value (equals 0 or 7) The value 0 seems to be used for the root folder.
N bytes Char[] Folder name. (Unicode null-terminated text)
4 bytes Spacing (Always equals 0)
4 bytes Int32? Block size (Always equals 20 so it equals the block header size)
4 bytes Int32 The number of sub-entries (files and folders)
'''
def __init__(self, rootind, name, entries):
self.entrytype = struct.pack('>H', 1) #Entry type (equals 1)
self.rootind = struct.pack('>I', rootind) #Unknown value (equals 0 or 7) The value 0 seems to be used for the root folder.
self.name = b'\x00' + name.encode('utf-16')[2:] + b'\x00'
self.spacing1 = struct.pack('>I', 0) #Spacing (Always equals 0)
self.size = struct.pack('>I', 20) #Block size (Always equals 20 so it equals the block header size)
self.entries = struct.pack('>I',entries) #The number of sub-entries (files and folders)
def string(self):
out = b''.join([self.entrytype, self.rootind, self.name, self.spacing1, self.size, self.entries])
return out
class LIFFileEntry:
'''
SIZE TYPE DESCRIPTION
2 bytes Int16 Entry type (equals 2)
4 bytes Int32 Spacing/unknown value (0 or 7)
N bytes Char[] File name. (Unicode null-terminated text)
4 bytes Spacing (Always equals 0)
4 bytes Int32 File size (it is actually the block size because it includes the block header size (20))
8 bytes Long Created, modified or accessed date
8 bytes Long Created, modified or accessed date
8 bytes Long Created, modified or accessed date
'''
def __init__(self, name, size):
self.entrytype = struct.pack('>H', 2) #Entry type (equals 2)
self.unknwown = struct.pack('>I', 7) #Spacing/unknown value (0 or 7).
self.name = b'\x00' + name.encode('utf-16')[2:] + b'\x00'
self.spacing1 = struct.pack('>I', 0) #Spacing (Always equals 0)
self.size = struct.pack('>I', size) #File size (it is actually the block size because it includes the block header size (20))
self.created = struct.pack('>Q', 18369614221190020847) #Created, modified or accessed date
self.modified = struct.pack('>Q', 18369614221190020847) #Created, modified or accessed date. Set to 0xFEEDFACECAFEBEEF for testing
self.accessed = b'\x01\xce\xec\xee\x85\x3b\x50\xdb' #Created, modified or accessed date
def string(self):
out = b''.join([self.entrytype, self.unknwown, self.name, self.spacing1, self.size, self.created, self.modified, self.accessed])
return out
def createLif(walk_dir):
outfile = os.path.basename(os.path.normpath(walk_dir))
number_of_files = 0
number_of_subdirs = 0
print('LIF Creator 1.1a')
print('Choosen directory: {0}'.format(os.path.normpath(walk_dir)))
#Create the first non-existant file to write to.
if os.path.exists(outfile + '.lif'):
i = 1
while(os.path.exists(outfile + "_" + str(i) + '.lif')):
i += 1
outfile = outfile + "_" + str(i)
start_time = time.time()
fi_content_str = b''
fo_dict = {}
fh_dict = {}
for root, subdirs, files in os.walk(walk_dir, topdown=False):
#ignore hidden files and folders (starting with a dot .)
files = [f for f in files if not f[0] == '.']
subdirs[:] = [d for d in subdirs if not d[0] == '.']
files_content_str = b''
files_fh_str = b''
for filename in files:
file_path = os.path.join(root, filename)
sys.stdout.write('\tPROCESSING: {0} \r'.format(filename))
sys.stdout.flush()
#print('Processing: {0}'.format(file_path))
with open(file_path, 'rb') as f:
current_data = f.read()
currenFileBlock = LIFBlock(blocktype=4, data=current_data) #Content of single file: Block Type 4
currenFileEntry = LIFFileEntry(name=filename, size=currenFileBlock.getSize())
fi_content_str = currenFileBlock.string()
fh_content_str = currenFileEntry.string()
currenFileBlock, currenFileEntry = None, None
files_content_str = b''.join([files_content_str, fi_content_str]) #Content of all files in current folder
files_fh_str = b''.join([files_fh_str, fh_content_str])
subfolders_content_str = b''
subfolders_fh_str = b''
for subdir in subdirs:
subfolders_content_str = b''.join([subfolders_content_str, fo_dict[os.path.join(root, subdir)]])
subfolders_fh_str = b''.join([subfolders_fh_str, fh_dict[os.path.join(root, subdir)]])
fo_dict.pop(os.path.join(root, subdir), None) #Drop item from fo dict, its no longer needed
fh_dict.pop(os.path.join(root, subdir), None) #Drop item from fh dict, its no longer needed
currenBlock = LIFBlock(blocktype=3, data=b''.join([files_content_str, subfolders_content_str]) ) #Block Type 3
number_entries = len(files) + len(subdirs)
currenDirEntry = LIFDirEntry(rootind=7, name=os.path.basename(root) , entries=number_entries)
fo_dict[root] = currenBlock.string()
fh_dict[root] = b''.join([currenDirEntry.string(), files_fh_str, subfolders_fh_str])
number_of_files = number_of_files + len(files)
number_of_subdirs = number_of_subdirs + len(subdirs)
'''Root directory block (Block Type 3)'''
#rootDirBlock = LIFBlock(blocktype=3, data=fo_dict[root])
rootDirBlock= fo_dict[root]
'''File Content Block (Block Type 2)'''
fileContentBlock = LIFBlock(blocktype=2, data='')
'''File hierarchy (Block Type 5)'''
fhBlock = LIFBlock(blocktype=5, data=fh_dict[root])
'''Root Block (Block Type 1)'''
rootBlock = LIFBlock(blocktype=1, data=b''.join([fileContentBlock.string(), rootDirBlock, fhBlock.string()]))
'''Header'''
headerBlock = LIFHeader()
headerBlock.setSize(len(rootBlock.string()) + 18)
print('\n\tCOMPLETED: {0} files in {1} directories processed. Writing {2}.lif now.'.format(str(number_of_files), str(number_of_subdirs), outfile))
lif_file = open((outfile + '.lif'), "wb")
lif_file.write(headerBlock.string())
lif_file.write(rootBlock.string())
lif_file.close()
print("--- %s seconds ---" % (time.time() - start_time))
if(len(sys.argv) > 1):
for i in range(1, len(sys.argv)):
createLif(sys.argv[i])
else:
print("LIF Creator 1.1a\n\nThis program will create LIF archives from an adjacent folder.\n\nCOPYRIGHT:\n\t(C) 2020 sttng\n\nLICENSE:\n\tGNU GPLv3\n\tYou accept full responsibility for how you use this program.\n\nUSEAGE:\n\t" + runCommand + " ")