# patchloader.py
# PatchLoader class
#
# Jiyong Jang, 2012
#
import os
import re
import time
import mimetypes
import common
class PatchLoader(object):
def __init__(self):
self._patch_list = []
self._npatch = 0
def traverse(self, patch_path):
'''
Traverse patch files
'''
print('[+] traversing patch files')
start_time = time.time()
if os.path.isfile(patch_path):
magic_type = common.file_type(patch_path)
common.verbose_print(' [-] %s: %s' % (patch_path, magic_type))
if magic_type.startswith('text'):
main_type, sub_type = magic_type.split('/')
self._process(patch_path)
elif os.path.isdir(patch_path):
for root,dirs,files in os.walk(patch_path):
for file in files:
file_path = os.path.join(root, file)
magic_type = common.file_type(file_path)
common.verbose_print(' [-] %s: %s' % (file_path, magic_type))
if magic_type.startswith('text'):
main_type, sub_type = magic_type.split('/')
self._process(file_path)
self._npatch = len(self._patch_list)
elapsed_time = time.time() - start_time
print('[+] %d patches ... %.1fs\n' % (self._npatch, elapsed_time))
return self._npatch
def _process(self, patch_path):
'''
Normalize a patch file and build a hash list
'''
patch_filename = patch_path.split('/')[-1]
patch_file = open(patch_path, 'r')
patch_lines = patch_file.readlines()
patch_file.close()
magic_ext = None
process_flag = False
diff_file = ''
diff_cnt = 0
diff_vuln_lines = []
diff_orig_lines = []
for line in patch_lines:
if line.startswith('--- '):
if diff_vuln_lines:
diff_norm_lines = self._normalize(''.join(diff_vuln_lines), magic_ext).split()
if len(diff_norm_lines) >= common.ngram_size:
common.verbose_print(' [-] %s %d (ext: %d)' % (diff_file, diff_cnt, magic_ext))
path = '[%s] %s #%d' % (patch_filename, diff_file, diff_cnt)
hash_list = self._build_hash_list(diff_norm_lines)
self._patch_list.append(common.PatchInfo(path, magic_ext, ''.join(diff_orig_lines), diff_norm_lines, hash_list))
else:
common.verbose_print(' [-] %s %d (ext: %d) - skipped (%d lines)' % (diff_file, diff_cnt, magic_ext, len(diff_norm_lines)))
del diff_vuln_lines[:]
del diff_orig_lines[:]
diff_path = line.split()[1]
if diff_path == '/dev/null':
process_flag = False
else:
process_flag = True
diff_cnt = 0
diff_file = diff_path.split('/')[-1]
magic_ext = self._get_file_type(diff_file)
elif process_flag:
# exclude the line '--' at the end of `git format-patch -1`
if line.startswith('--'):
continue
if line.startswith('+++ '):
diff_path = line.split()[1]
if diff_path == '/dev/null':
process_flag = False
elif line.startswith('@@'):
if diff_vuln_lines:
diff_norm_lines = self._normalize(''.join(diff_vuln_lines), magic_ext).split()
if len(diff_norm_lines) >= common.ngram_size:
common.verbose_print(' [-] %s %d (ext: %d)' % (diff_file, diff_cnt, magic_ext))
path = '[%s] %s #%d' % (patch_filename, diff_file, diff_cnt)
hash_list = self._build_hash_list(diff_norm_lines)
self._patch_list.append(common.PatchInfo(path, magic_ext, ''.join(diff_orig_lines), diff_norm_lines, hash_list))
else:
common.verbose_print(' [-] %s %d (ext: %d) - skipped (%d lines)' % (diff_file, diff_cnt, magic_ext, len(diff_norm_lines)))
del diff_vuln_lines[:]
del diff_orig_lines[:]
diff_cnt += 1
elif line.startswith('-'):
diff_vuln_lines.append(line[1:])
diff_orig_lines.append('')
diff_orig_lines.append(line.replace('','>'))
diff_orig_lines.append('')
elif line.startswith('+'):
diff_orig_lines.append('')
diff_orig_lines.append(line.replace('','>'))
diff_orig_lines.append('')
elif line.startswith(' '):
diff_vuln_lines.append(line[1:])
diff_orig_lines.append(line.replace('','>'))
if diff_vuln_lines:
diff_norm_lines = self._normalize(''.join(diff_vuln_lines), magic_ext).split()
if len(diff_norm_lines) >= common.ngram_size:
common.verbose_print(' [-] %s %d (ext: %d)' % (diff_file, diff_cnt, magic_ext))
path = '[%s] %s #%d' % (patch_filename, diff_file, diff_cnt)
hash_list = self._build_hash_list(diff_norm_lines)
self._patch_list.append(common.PatchInfo(path, magic_ext, ''.join(diff_orig_lines), diff_norm_lines, hash_list))
else:
common.verbose_print(' [-] %s %d (ext: %d) - skipped (%d lines)' % (diff_file, diff_cnt, magic_ext, len(diff_norm_lines)))
def _normalize(self, patch, ext):
'''
Normalize a patch file
'''
# Language-specific optimization
if ext==common.FileExt.C or ext==common.FileExt.Java:
patch = ''.join([c.group('noncomment') for c in common.c_regex.finditer(patch) if c.group('noncomment')])
patch = ''.join([c.group('noncomment') for c in common.c_partial_comment_regex.finditer(patch) if c.group('noncomment')])
elif ext==common.FileExt.ShellScript or ext==common.FileExt.Python:
patch = ''.join([c.group('noncomment') for c in common.shellscript_regex.finditer(patch) if c.group('noncomment')])
elif ext==common.FileExt.Perl:
patch = ''.join([c.group('noncomment') for c in common.perl_regex.finditer(patch) if c.group('noncomment')])
elif ext==common.FileExt.PHP:
patch = ''.join([c.group('noncomment') for c in common.php_regex.finditer(patch) if c.group('noncomment')])
patch = ''.join([c.group('noncomment') for c in common.c_partial_comment_regex.finditer(patch) if c.group('noncomment')])
elif ext==common.FileExt.Ruby:
patch = ''.join([c.group('noncomment') for c in common.ruby_regex.finditer(patch) if c.group('noncomment')])
patch = ''.join([c.group('noncomment') for c in common.ruby_partial_comment_regex.finditer(patch) if c.group('noncomment')])
# Remove whitespaces except newlines
patch = common.whitespaces_regex.sub("", patch)
# Convert into lowercases
return patch.lower()
def _build_hash_list(self, diff_norm_lines):
'''
Build a hash list
'''
hash_list = []
num_ngram = len(diff_norm_lines) - common.ngram_size + 1
for i in range(0, num_ngram):
ngram = ''.join(diff_norm_lines[i:i+common.ngram_size])
hash1 = common.fnv1a_hash(ngram) & (common.bloomfilter_size-1)
hash2 = common.djb2_hash(ngram) & (common.bloomfilter_size-1)
hash3 = common.sdbm_hash(ngram) & (common.bloomfilter_size-1)
hash_list.append(hash1)
hash_list.append(hash2)
hash_list.append(hash3)
return hash_list
def _get_file_type(self, file_path):
'''
Guess a file type based upon a file extension (mimetypes module)
'''
file_type, encoding = mimetypes.guess_type(file_path)
magic_ext = None
if file_type is None:
magic_ext = common.FileExt.Text
else:
main_type, sub_type = file_type.split('/')
if sub_type.startswith('x-c'):
magic_ext = common.FileExt.C
elif sub_type == 'x-java':
magic_ext = common.FileExt.Java
elif sub_type == 'x-sh':
magic_ext = common.FileExt.ShellScript
elif sub_type == 'x-perl':
magic_ext = common.FileExt.Perl
elif sub_type == 'x-python':
magic_ext = common.FileExt.Python
elif sub_type == 'x-httpd-php':
magic_ext = common.FileExt.PHP
elif sub_type == 'x-ruby':
magic_ext = common.FileExt.Ruby
else:
magic_ext = common.FileExt.Text
return magic_ext
def items(self):
return self._patch_list
def length(self):
return self._npatch