New metadata scanner

This commit is contained in:
Ruud
2012-01-27 18:54:12 +01:00
parent fe7ac58bf5
commit 228f7e9e9e
192 changed files with 193 additions and 44918 deletions
+23 -17
View File
@@ -5,15 +5,17 @@ from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File
from couchpotato.environment import Env
from flask.helpers import json
from enzyme.exceptions import NoParserError
import enzyme
import logging
import os
import re
import subprocess
import sys
import time
import traceback
enzyme_logger = logging.getLogger('enzyme')
enzyme_logger.setLevel(logging.INFO)
log = CPLog(__name__)
@@ -307,11 +309,12 @@ class Scanner(Plugin):
meta = self.getMeta(cur_file)
try:
data['video'] = self.getCodec(cur_file, self.codecs['video'])
data['audio'] = meta['audio stream'][0]['compression']
data['resolution_width'] = meta['video stream'][0]['image width']
data['resolution_height'] = meta['video stream'][0]['image height']
data['video'] = meta.get('video', self.getCodec(cur_file, self.codecs['video']))
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
data['resolution_width'] = meta.get('resolution_width', 720)
data['resolution_height'] = meta.get('resolution_height', 480)
except:
log.debug('Error parsing metadata: %s %s' % (cur_file, traceback.format_exc()))
pass
if data.get('audio'): break
@@ -329,17 +332,20 @@ class Scanner(Plugin):
return data
def getMeta(self, filename):
lib_dir = os.path.join(Env.get('app_dir'), 'libs')
script = os.path.join(lib_dir, 'getmeta.py')
p = subprocess.Popen([sys.executable, script, filename], stdout = subprocess.PIPE, stderr = subprocess.PIPE, cwd = lib_dir)
z = p.communicate()[0]
try:
meta = json.loads(z)
return meta
except Exception:
log.error('Couldn\'t get metadata from file: %s' % traceback.format_exc())
p = enzyme.parse(filename)
return {
'video': p.video[0].codec,
'audio': p.audio[0].codec,
'resolution_width': p.video[0].width,
'resolution_height': p.video[0].height,
}
except NoParserError:
log.debug('No parser found for %s' % filename)
return {}
def determineMovie(self, group):
imdb_id = None
+6 -6
View File
@@ -64,7 +64,7 @@ class Media(object):
_keys = MEDIACORE
table_mapping = {}
def __init__(self, hash=None):
def __init__(self, hash = None):
if hash is not None:
# create Media based on dict
for key, value in hash.items():
@@ -253,7 +253,7 @@ class Media(object):
"""
return hasattr(self, key)
def get(self, attr, default=None):
def get(self, attr, default = None):
"""
Returns the given attribute. If the attribute is not set by
the parser return 'default'.
@@ -315,7 +315,7 @@ class Tag(object):
Tag values are strings (for binary data), unicode objects, or datetime
objects for tags that represent dates or times.
"""
def __init__(self, value=None, langcode='und', binary=False):
def __init__(self, value = None, langcode = 'und', binary = False):
super(Tag, self).__init__()
self.value = value
self.langcode = langcode
@@ -363,7 +363,7 @@ class Tags(dict, Tag):
The attribute RATING has a value (PG), but it also has a child tag
COUNTRY that specifies the country code the rating belongs to.
"""
def __init__(self, value=None, langcode='und', binary=False):
def __init__(self, value = None, langcode = 'und', binary = False):
super(Tags, self).__init__()
self.value = value
self.langcode = langcode
@@ -410,7 +410,7 @@ class Chapter(Media):
"""
_keys = ['enabled', 'name', 'pos', 'id']
def __init__(self, name=None, pos=0):
def __init__(self, name = None, pos = 0):
Media.__init__(self)
self.name = name
self.pos = pos
@@ -424,7 +424,7 @@ class Subtitle(Media):
_keys = ['enabled', 'default', 'langcode', 'language', 'trackno', 'title',
'id', 'codec']
def __init__(self, language=None):
def __init__(self, language = None):
Media.__init__(self)
self.language = language
-205
View File
@@ -1,205 +0,0 @@
from flask.helpers import json
from hachoir_core.cmd_line import unicodeFilename
from hachoir_metadata import extractMetadata
from hachoir_parser import createParser
import datetime
import re
import sys
def getMetadata(filename):
filename, realname = unicodeFilename(filename), filename
parser = createParser(filename, realname)
try:
metadata = extractMetadata(parser)
except:
return None
if metadata is not None:
metadata = metadata.exportPlaintext()
return metadata
return None
def parseMetadata(meta, jsonsafe=True):
'''
Return a dict of section headings like 'Video stream' or 'Audio stream'. Each key will have a list of dicts.
This supports multiple video/audio/subtitle/whatever streams per stream type. Each element in the list of streams
will he a dict with keys like 'Image height' and 'Compression'...anything that hachoir is able to extract.
An example output:
{'Audio stream': [{u'Channel': u'6',
u'Compression': u'A_AC3',
u'Sample rate': u'48.0 kHz'}],
u'Common': [{u'Creation date': u'2008-03-20 09:09:43',
u'Duration': u'1 hour 40 min 6 sec',
u'Endianness': u'Big endian',
u'MIME type': u'video/x-matroska',
u'Producer': u'libebml v0.7.7 + libmatroska v0.8.1'}],
'Video stream': [{u'Compression': u'V_MPEG4/ISO/AVC',
u'Image height': u'688 pixels',
u'Image width': u'1280 pixels',
u'Language': u'English'}]}
'''
if not meta:
return
sections = {}
what = []
for line in meta:
#if line doesn't start with "- " it is a section heading
if line[:2] != "- ":
section = line.strip(":").lower()
#lets collapse multiple stream headings into one...
search = re.search(r'#\d+\Z', section)
if search:
section = re.sub(search.group(), '', section).strip()
if section not in sections:
sections[section] = [dict()]
else:
sections[section].append(dict())
else:
#This isn't a section heading, so we put it in the last section heading we found.
#meta always starts out with a section heading so 'section' will always be defined
i = line.find(":")
key = line[2:i].lower()
value = _parseValue(section, key, line[i+2:])
if value is None:
value = line[i+2:]
if jsonsafe:
try:
v = json.dumps(value)
except TypeError:
value = str(value)
sections[section][-1][key] = value
return sections
def _parseValue(section, key, value, jsonsafe = True):
'''
Tediously check all the types that we know about (checked over 7k videos to find these)
and convert them to python native types.
If jsonsafe is True, we'll make json-unfriendly types like datetime into json-friendly.
'''
date_search = re.search("\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d", value)
if key == 'bit rate':
ret = _parseBitRate(value.lower())
elif key == 'bits/sample' or key == 'bits/pixel':
try:
bits = int(value.split()[0])
ret = bits
except:
ret = None
elif key == 'channel':
if value == 'stereo':
ret = 2
elif value == 'mono':
ret = 1
else:
try:
channels = int(value)
ret = channels
except:
ret = None
elif key == 'compression':
ret = _parseCompression(value)
elif key == 'compression rate':
try:
ret = float(value.split('x')[0])
except:
ret = None
elif key == 'duration':
try:
ret = _parseDuration(value)
except:
ret = None
elif key == 'sample rate':
try:
ret = float(value.split()[0]) * 1000
except:
ret = None
elif key == 'frame rate':
try:
ret = float(value.split()[0])
except:
pass
elif key == 'image height' or key == 'image width':
pixels = re.match("(?P<pixels>\d{1,4}) pixel", value)
if pixels:
ret = int(pixels.group('pixels'))
else:
ret = None
elif date_search:
try:
ret = datetime.datetime.strptime(date_search.group(), "%Y-%m-%d %H:%M:%S")
except:
ret = None
else:
#If it's something we don't know about...
ret = None
return ret
def _parseDuration(value):
t = re.search(r"((?P<hour>\d+) hour(s|))? ?((?P<min>\d+) min)? ?((?P<sec>\d+) sec)? ?((?P<ms>\d+) ms)?", value)
if t:
hour = 0 if not t.group('hour') else int(t.group('hour'))
min = 0 if not t.group('min') else int(t.group('min'))
sec = 0 if not t.group('sec') else int(t.group('sec'))
ms = 0 if not t.group('ms') else int(t.group('ms'))
return datetime.timedelta(hours = hour, minutes = min, seconds = sec, milliseconds = ms)
def _parseCompression(value):
codecs = {
'v_mpeg4/iso/avc': 'AVC',
'x264': 'AVC',
'divx': 'divx',
'xvid': 'xvid',
'v_ms/vfw/fourcc': 'vfw',
'vorbis': 'vorbis',
'xvid': 'xvid',
'mpeg layer 3': 'mp3',
'a_dts': 'DTS',
'a_aac': 'AAC',
'a_truehd': 'TRUEHD',
'microsoft mpeg': 'MPEG',
'ac3': 'AC3',
'wvc1': 'WVC1',
'pulse code modulation': 'PCM',
'pcm': 'PCM',
'windows media audio': 'WMA',
'windows media video': 'WMV',
's_text/ascii': 'ASCII',
's_text/utf8': 'UTF8',
's_text/ssa': 'SSA',
's_text/ass': 'ASS'
}
for codec in codecs:
if codec in value.lower():
return codecs[codec]
def _parseBitRate(value):
try:
bitrate = float(value.split()[0])
except:
return None
if 'kbit' in value.lower():
multi = 1000
elif 'mbit' in value.lower():
multi = 1000 * 1000
else:
return None
return bitrate * multi
print json.dumps(parseMetadata(getMetadata(sys.argv[1])))
+4 -1
View File
@@ -18,7 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = '0.2'
__version__ = '0.3-dev'
__all__ = [ 'Guess', 'Language',
'guess_file_info', 'guess_video_info',
'guess_movie_info', 'guess_episode_info' ]
@@ -52,6 +52,9 @@ def guess_file_info(filename, filetype, info = [ 'filename' ]):
result = []
hashers = []
if isinstance(info, basestring):
info = [ info ]
for infotype in info:
if infotype == 'filename':
m = IterativeMatcher(filename, filetype = filetype)
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# GuessIt - A library for guessing information from filenames
# Copyright (c) 2011 Nicolas Wack <wackou@gmail.com>
#
# GuessIt is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# GuessIt 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
# Lesser GNU General Public License for more details.
#
# You should have received a copy of the Lesser GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from guessit.patterns import subtitle_exts, video_exts, episode_rexps, find_properties, canonical_form
import os.path
import re
import logging
log = logging.getLogger("guessit.filetype")
def guess_filetype(filename, filetype = 'autodetect'):
other = {}
# look at the extension first
fileext = os.path.splitext(filename)[1][1:].lower()
if fileext in subtitle_exts:
if 'movie' in filetype:
filetype = 'moviesubtitle'
elif 'episode' in filetype:
filetype = 'episodesubtitle'
else:
filetype = 'subtitle'
other = { 'container': fileext }
elif fileext in video_exts:
if filetype == 'autodetect':
filetype = 'video'
other = { 'container': fileext }
else:
if filetype == 'autodetect':
filetype = 'unknown'
other = { 'extension': fileext }
# now look whether there are some specific hints for episode vs movie
if filetype in ('video', 'subtitle'):
for rexp, confidence, span_adjust in episode_rexps:
match = re.search(rexp, filename, re.IGNORECASE)
if match:
if filetype == 'video':
filetype = 'episode'
elif filetype == 'subtitle':
filetype = 'episodesubtitle'
break
for prop, value, start, end in find_properties(filename):
if canonical_form(value) == 'DVB':
if filetype == 'video':
filetype = 'episode'
elif filetype == 'subtitle':
filetype = 'episodesubtitle'
break
# if no episode info found, assume it's a movie
if filetype == 'video':
filetype = 'movie'
elif filetype == 'subtitle':
filetype = 'moviesubtitle'
return filetype, other
+16 -1
View File
@@ -18,7 +18,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import ntpath
import os.path
import zipfile
def split_path(path):
@@ -44,7 +46,7 @@ def split_path(path):
"""
result = []
while True:
head, tail = os.path.split(path)
head, tail = ntpath.split(path)
# on Unix systems, the root folder is '/'
if head == '/' and tail == '':
@@ -81,3 +83,16 @@ def file_in_same_dir(ref_file, desired_file):
"""
return os.path.join(*(split_path(ref_file)[:-1] + [ desired_file ]))
def load_file_in_same_dir(ref_file, filename):
"""Load a given file. Works even when the file is contained inside a zip."""
path = split_path(ref_file)[:-1] + [ filename ]
for i, p in enumerate(path):
if p.endswith('.zip'):
zfilename = os.path.join(*path[:i+1])
zfile = zipfile.ZipFile(zfilename)
return zfile.read('/'.join(path[i+1:]))
return open(os.path.join(*path)).read()
+2 -1
View File
@@ -33,7 +33,8 @@ log = logging.getLogger('guessit.language')
# "An alpha-3 (bibliographic) code, an alpha-3 (terminologic) code (when given),
# an alpha-2 code (when given), an English name, and a French name of a language
# are all separated by pipe (|) characters."
language_matrix = [ l.strip().decode('utf-8').split('|') for l in open(fileutils.file_in_same_dir(__file__, 'ISO-639-2_utf-8.txt')) ]
language_matrix = [ l.strip().decode('utf-8').split('|')
for l in fileutils.load_file_in_same_dir(__file__, 'ISO-639-2_utf-8.txt').split('\n') ]
lng3 = frozenset(filter(bool, (l[0] for l in language_matrix)))
lng3term = frozenset(filter(bool, (l[1] for l in language_matrix)))
+32 -55
View File
@@ -3,6 +3,7 @@
#
# GuessIt - A library for guessing information from filenames
# Copyright (c) 2011 Nicolas Wack <wackou@gmail.com>
# Copyright (c) 2011 Ricard Marxer <ricardmp@gmail.com>
#
# GuessIt is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
@@ -22,7 +23,8 @@ from guessit import fileutils, textutils
from guessit.guess import Guess, merge_similar_guesses, merge_all, choose_int, choose_string
from guessit.date import search_date, search_year
from guessit.language import search_language
from guessit.patterns import video_exts, subtitle_exts, sep, deleted, video_rexps, websites, episode_rexps, weak_episode_rexps, non_episode_title, properties, canonical_form
from guessit.filetype import guess_filetype
from guessit.patterns import video_exts, subtitle_exts, sep, deleted, video_rexps, websites, episode_rexps, weak_episode_rexps, non_episode_title, find_properties, canonical_form, unlikely_series
from guessit.matchtree import get_group, find_group, leftover_valid_groups, tree_to_string
from guessit.textutils import find_first_level_groups, split_on_groups, blank_region, clean_string, to_utf8
from guessit.fileutils import split_path_components
@@ -31,6 +33,7 @@ import os.path
import re
import copy
import logging
import mimetypes
log = logging.getLogger("guessit.matcher")
@@ -148,22 +151,11 @@ def guess_groups(string, result, filetype):
# common well-defined words and regexps
clow = current.lower()
confidence = 1.0 # for all of them
for prop, values in properties.items():
for value in values:
pos = clow.find(value.lower())
if pos != -1:
end = pos + len(value)
# make sure our word is always surrounded by separators
if clow[pos-1] not in sep or clow[end] not in sep:
# note: sep is a regexp, but in this case using it as
# a sequence achieves the same goal
continue
for prop, value, pos, end in find_properties(current):
guess = guessed({ prop: value }, confidence = confidence)
current = update_found(current, guess, (pos, end))
guess = guessed({ prop: value }, confidence = confidence)
current = update_found(current, guess, (pos, end))
clow = current.lower()
# weak guesses for episode number, only run it if we don't have an estimate already
if filetype in ('episode', 'episodesubtitle'):
@@ -341,9 +333,10 @@ class IterativeMatcher(object):
resolution when they arise.
"""
if filetype not in ('autodetect', 'subtitle', 'movie', 'moviesubtitle',
if filetype not in ('autodetect', 'subtitle', 'video',
'movie', 'moviesubtitle',
'episode', 'episodesubtitle'):
raise ValueError, "filetype needs to be one of ('autodetect', 'subtitle', 'movie', 'moviesubtitle', 'episode', 'episodesubtitle')"
raise ValueError, "filetype needs to be one of ('autodetect', 'subtitle', 'video', 'movie', 'moviesubtitle', 'episode', 'episodesubtitle')"
if not isinstance(filename, unicode):
log.debug('WARNING: given filename to matcher is not unicode...')
@@ -368,43 +361,21 @@ class IterativeMatcher(object):
# 1- first split our path into dirs + basename + ext
match_tree = split_path_components(filename)
fileext = match_tree.pop(-1)[1:].lower()
if fileext in subtitle_exts:
if 'movie' in filetype:
filetype = 'moviesubtitle'
elif 'episode' in filetype:
filetype = 'episodesubtitle'
else:
filetype = 'subtitle'
extguess = guessed({ 'container': fileext }, confidence = 1.0)
elif fileext in video_exts:
extguess = guessed({ 'container': fileext }, confidence = 1.0)
else:
extguess = guessed({ 'extension': fileext}, confidence = 1.0)
# TODO: depending on the extension, we could already grab some info and maybe specialized
# guessers, eg: a lang parser for idx files, an automatic detection of the language
# for srt files, a video metadata extractor for avi, mkv, ...
# if we are on autodetect, try to do it now so we can tell the
# guess_groups function what type of info it should be looking for
if filetype in ('autodetect', 'subtitle'):
for rexp, confidence, span_adjust in episode_rexps:
match = re.search(rexp, filename, re.IGNORECASE)
if match:
if filetype == 'autodetect':
filetype = 'episode'
elif filetype == 'subtitle':
filetype = 'episodesubtitle'
break
# if no episode info found, assume it's a movie
if filetype == 'autodetect':
filetype = 'movie'
elif filetype == 'subtitle':
filetype = 'moviesubtitle'
# try to detect the file type
filetype, other = guess_filetype(filename, filetype)
guessed({ 'type': filetype }, confidence = 1.0)
extguess = guessed(other, confidence = 1.0)
# guess the mimetype of the filename
# TODO: handle other mimetypes not found on the default type_maps
# mimetypes.types_map['.srt']='text/subtitle'
mime, _ = mimetypes.guess_type(filename, strict=False)
if mime is not None:
guessed({ 'mimetype': mime }, confidence = 1.0)
# remove the extension from the match tree, as all indices relative
# the the filename groups assume the basename is the last one
fileext = match_tree.pop(-1)[1:].lower()
# 2- split each of those into explicit groups, if any
@@ -453,8 +424,14 @@ class IterativeMatcher(object):
if len(previous) == 1:
guess = guessed({ 'series': previous[0][0] }, confidence = 0.5)
leftover = update_found(leftover, previous[0][1], guess)
# reduce the confidence of unlikely series
for guess in result:
if 'series' in guess:
if guess['series'].lower() in unlikely_series:
guess.set_confidence('series', guess.confidence('series') * 0.5)
elif filetype in ('movie', 'moviesubtitle'):
leftover_all = leftover_valid_groups(match_tree)
+34 -7
View File
@@ -3,6 +3,7 @@
#
# GuessIt - A library for guessing information from filenames
# Copyright (c) 2011 Nicolas Wack <wackou@gmail.com>
# Copyright (c) 2011 Ricard Marxer <ricardmp@gmail.com>
#
# GuessIt is free software; you can redistribute it and/or modify it under
# the terms of the Lesser GNU General Public License as published by
@@ -19,9 +20,9 @@
#
subtitle_exts = [ 'srt', 'idx', 'sub' ]
subtitle_exts = [ 'srt', 'idx', 'sub', 'ssa', 'txt' ]
video_exts = [ 'avi', 'mkv', 'mpg', 'mp4', 'mov', 'ogg', 'ogm', 'ogv', 'wmv' ]
video_exts = [ 'avi', 'mkv', 'mpg', 'mp4', 'm4v', 'mov', 'ogg', 'ogm', 'ogv', 'wmv', 'divx' ]
# separator character regexp
sep = r'[][)(}{+ \._-]' # regexp art, hehe :D
@@ -34,6 +35,9 @@ episode_rexps = [ # ... Season 2 ...
(r'season (?P<season>[0-9]+)', 1.0, (0, 0)),
(r'saison (?P<season>[0-9]+)', 1.0, (0, 0)),
# ... s02-x01 ...
(r's(?P<season>[0-9]{1,2})-x(?P<bonusNumber>[0-9]{1,2})[^0-9]', 1.0, (0, -1)),
# ... s02e13 ...
(r'[Ss](?P<season>[0-9]{1,2}).{,3}[EeXx](?P<episodeNumber>[0-9]{1,2})[^0-9]', 1.0, (0, -1)),
@@ -41,7 +45,7 @@ episode_rexps = [ # ... Season 2 ...
(r'[^0-9](?P<season>[0-9]{1,2})[x\.](?P<episodeNumber>[0-9]{2})[^0-9]', 0.8, (1, -1)),
# ... s02 ...
(sep + r's(?P<season>[0-9]{1,2})' + sep + '?', 0.6, (0, 0)),
(sep + r's(?P<season>[0-9]{1,2})' + sep + '?', 0.6, (1, -1)),
# v2 or v3 for some mangas which have multiples rips
(sep + r'(?P<episodeNumber>[0-9]{1,3})v[23]' + sep, 0.6, (0, 0)),
@@ -77,11 +81,11 @@ video_rexps = [ # cd number
websites = [ 'tvu.org.ru', 'emule-island.com', 'UsaBit.com', 'www.divx-overnet.com', 'sharethefiles.com' ]
properties = { 'format': [ 'DVDRip', 'HD-DVD', 'HDDVD', 'HDDVDRip', 'BluRay', 'Blu-ray', 'BDRip', 'BRRip',
'HDRip', 'DVD', 'DVDivX', 'HDTV', 'DVB', 'WEBRip', 'DVDSCR', 'Screener', 'VHS',
'VIDEO_TS' ],
unlikely_series = ['series']
'container': [ 'avi', 'mkv', 'ogv', 'ogm', 'wmv', 'mp4', 'mov' ],
properties = { 'format': [ 'DVDRip', 'HD-DVD', 'HDDVD', 'HDDVDRip', 'BluRay', 'Blu-ray', 'BDRip', 'BRRip',
'HDRip', 'DVD', 'DVDivX', 'HDTV', 'DVB', 'DVBRip', 'PDTV', 'WEBRip',
'DVDSCR', 'Screener', 'VHS', 'VIDEO_TS' ],
'screenSize': [ '720p', '720' ],
@@ -106,10 +110,29 @@ properties = { 'format': [ 'DVDRip', 'HD-DVD', 'HDDVD', 'HDDVDRip', 'BluRay', 'B
],
}
def find_properties(filename):
result = []
clow = filename.lower()
for prop, values in properties.items():
for value in values:
pos = clow.find(value.lower())
if pos != -1:
end = pos + len(value)
# make sure our word is always surrounded by separators
if ((pos > 0 and clow[pos-1] not in sep) or
(end < len(clow) and clow[end] not in sep)):
# note: sep is a regexp, but in this case using it as
# a sequence achieves the same goal
continue
result.append((prop, value, pos, end))
return result
property_synonyms = { 'DVD': [ 'DVDRip', 'VIDEO_TS' ],
'HD-DVD': [ 'HDDVD', 'HDDVDRip' ],
'BluRay': [ 'BDRip', 'BRRip', 'Blu-ray' ],
'DVB': [ 'DVBRip', 'PDTV' ],
'Screener': [ 'DVDSCR' ],
'DivX': [ 'DVDivX' ],
'h264': [ 'x264' ],
@@ -123,6 +146,10 @@ property_synonyms = { 'DVD': [ 'DVDRip', 'VIDEO_TS' ],
reverse_synonyms = {}
for prop, values in properties.items():
for value in values:
reverse_synonyms[value.lower()] = value
for canonical, synonyms in property_synonyms.items():
for synonym in synonyms:
reverse_synonyms[synonym.lower()] = canonical
-2
View File
@@ -1,2 +0,0 @@
from hachoir_core.version import VERSION as __version__, PACKAGE, WEBSITE, LICENSE
-210
View File
@@ -1,210 +0,0 @@
from hachoir_core.tools import humanDurationNanosec
from hachoir_core.i18n import _
from math import floor
from time import time
class BenchmarkError(Exception):
"""
Error during benchmark, use str(err) to format it as string.
"""
def __init__(self, message):
Exception.__init__(self,
"Benchmark internal error: %s" % message)
class BenchmarkStat:
"""
Benchmark statistics. This class automatically computes minimum value,
maximum value and sum of all values.
Methods:
- append(value): append a value
- getMin(): minimum value
- getMax(): maximum value
- getSum(): sum of all values
- __len__(): get number of elements
- __nonzero__(): isn't empty?
"""
def __init__(self):
self._values = []
def append(self, value):
self._values.append(value)
try:
self._min = min(self._min, value)
self._max = max(self._max, value)
self._sum += value
except AttributeError:
self._min = value
self._max = value
self._sum = value
def __len__(self):
return len(self._values)
def __nonzero__(self):
return bool(self._values)
def getMin(self):
return self._min
def getMax(self):
return self._max
def getSum(self):
return self._sum
class Benchmark:
def __init__(self, max_time=5.0,
min_count=5, max_count=None, progress_time=1.0):
"""
Constructor:
- max_time: Maximum wanted duration of the whole benchmark
(default: 5 seconds, minimum: 1 second).
- min_count: Minimum number of function calls to get good statistics
(defaut: 5, minimum: 1).
- progress_time: Time between each "progress" message
(default: 1 second, minimum: 250 ms).
- max_count: Maximum number of function calls (default: no limit).
- verbose: Is verbose? (default: False)
- disable_gc: Disable garbage collector? (default: False)
"""
self.max_time = max(max_time, 1.0)
self.min_count = max(min_count, 1)
self.max_count = max_count
self.progress_time = max(progress_time, 0.25)
self.verbose = False
self.disable_gc = False
def formatTime(self, value):
"""
Format a time delta to string: use humanDurationNanosec()
"""
return humanDurationNanosec(value * 1000000000)
def displayStat(self, stat):
"""
Display statistics to stdout:
- best time (minimum)
- average time (arithmetic average)
- worst time (maximum)
- total time (sum)
Use arithmetic avertage instead of geometric average because
geometric fails if any value is zero (returns zero) and also
because floating point multiplication lose precision with many
values.
"""
average = stat.getSum() / len(stat)
values = (stat.getMin(), average, stat.getMax(), stat.getSum())
values = tuple(self.formatTime(value) for value in values)
print _("Benchmark: best=%s average=%s worst=%s total=%s") \
% values
def _runOnce(self, func, args, kw):
before = time()
func(*args, **kw)
after = time()
return after - before
def _run(self, func, args, kw):
"""
Call func(*args, **kw) as many times as needed to get
good statistics. Algorithm:
- call the function once
- compute needed number of calls
- and then call function N times
To compute number of calls, parameters are:
- time of first function call
- minimum number of calls (min_count attribute)
- maximum test time (max_time attribute)
Notice: The function will approximate number of calls.
"""
# First call of the benchmark
stat = BenchmarkStat()
diff = self._runOnce(func, args, kw)
best = diff
stat.append(diff)
total_time = diff
# Compute needed number of calls
count = int(floor(self.max_time / diff))
count = max(count, self.min_count)
if self.max_count:
count = min(count, self.max_count)
# Not other call? Just exit
if count == 1:
return stat
estimate = diff * count
if self.verbose:
print _("Run benchmark: %s calls (estimate: %s)") \
% (count, self.formatTime(estimate))
display_progress = self.verbose and (1.0 <= estimate)
total_count = 1
while total_count < count:
# Run benchmark and display each result
if display_progress:
print _("Result %s/%s: %s (best: %s)") % \
(total_count, count,
self.formatTime(diff), self.formatTime(best))
part = count - total_count
# Will takes more than one second?
average = total_time / total_count
if self.progress_time < part * average:
part = max( int(self.progress_time / average), 1)
for index in xrange(part):
diff = self._runOnce(func, args, kw)
stat.append(diff)
total_time += diff
best = min(diff, best)
total_count += part
if display_progress:
print _("Result %s/%s: %s (best: %s)") % \
(count, count,
self.formatTime(diff), self.formatTime(best))
return stat
def validateStat(self, stat):
"""
Check statistics and raise a BenchmarkError if they are invalid.
Example of tests: reject empty stat, reject stat with only nul values.
"""
if not stat:
raise BenchmarkError("empty statistics")
if not stat.getSum():
raise BenchmarkError("nul statistics")
def run(self, func, *args, **kw):
"""
Run function func(*args, **kw), validate statistics,
and display the result on stdout.
Disable garbage collector if asked too.
"""
# Disable garbarge collector is needed and if it does exist
# (Jython 2.2 don't have it for example)
if self.disable_gc:
try:
import gc
except ImportError:
self.disable_gc = False
if self.disable_gc:
gc_enabled = gc.isenabled()
gc.disable()
else:
gc_enabled = False
# Run the benchmark
stat = self._run(func, args, kw)
if gc_enabled:
gc.enable()
# Validate and display stats
self.validateStat(stat)
self.displayStat(stat)
-277
View File
@@ -1,277 +0,0 @@
"""
Utilities to convert integers and binary strings to binary (number), binary
string, number, hexadecimal, etc.
"""
from hachoir_core.endian import BIG_ENDIAN, LITTLE_ENDIAN
from hachoir_core.compatibility import reversed
from itertools import chain, repeat
from struct import calcsize, unpack, error as struct_error
def swap16(value):
"""
Swap byte between big and little endian of a 16 bits integer.
>>> "%x" % swap16(0x1234)
'3412'
"""
return (value & 0xFF) << 8 | (value >> 8)
def swap32(value):
"""
Swap byte between big and little endian of a 32 bits integer.
>>> "%x" % swap32(0x12345678)
'78563412'
"""
value = long(value)
return ((value & 0x000000FFL) << 24) \
| ((value & 0x0000FF00L) << 8) \
| ((value & 0x00FF0000L) >> 8) \
| ((value & 0xFF000000L) >> 24)
def bin2long(text, endian):
"""
Convert binary number written in a string into an integer.
Skip characters differents than "0" and "1".
>>> bin2long("110", BIG_ENDIAN)
6
>>> bin2long("110", LITTLE_ENDIAN)
3
>>> bin2long("11 00", LITTLE_ENDIAN)
3
"""
assert endian in (LITTLE_ENDIAN, BIG_ENDIAN)
bits = [ (ord(character)-ord("0")) \
for character in text if character in "01" ]
assert len(bits) != 0
if endian is not BIG_ENDIAN:
bits = reversed(bits)
value = 0
for bit in bits:
value *= 2
value += bit
return value
def str2hex(value, prefix="", glue=u"", format="%02X"):
r"""
Convert binary string in hexadecimal (base 16).
>>> str2hex("ABC")
u'414243'
>>> str2hex("\xF0\xAF", glue=" ")
u'F0 AF'
>>> str2hex("ABC", prefix="0x")
u'0x414243'
>>> str2hex("ABC", format=r"\x%02X")
u'\\x41\\x42\\x43'
"""
if isinstance(glue, str):
glue = unicode(glue)
if 0 < len(prefix):
text = [prefix]
else:
text = []
for character in value:
text.append(format % ord(character))
return glue.join(text)
def countBits(value):
"""
Count number of bits needed to store a (positive) integer number.
>>> countBits(0)
1
>>> countBits(1000)
10
>>> countBits(44100)
16
>>> countBits(18446744073709551615)
64
"""
assert 0 <= value
count = 1
bits = 1
while (1 << bits) <= value:
count += bits
value >>= bits
bits <<= 1
while 2 <= value:
if bits != 1:
bits >>= 1
else:
bits -= 1
while (1 << bits) <= value:
count += bits
value >>= bits
return count
def byte2bin(number, classic_mode=True):
"""
Convert a byte (integer in 0..255 range) to a binary string.
If classic_mode is true (default value), reverse bits.
>>> byte2bin(10)
'00001010'
>>> byte2bin(10, False)
'01010000'
"""
text = ""
for i in range(0, 8):
if classic_mode:
mask = 1 << (7-i)
else:
mask = 1 << i
if (number & mask) == mask:
text += "1"
else:
text += "0"
return text
def long2raw(value, endian, size=None):
r"""
Convert a number (positive and not nul) to a raw string.
If size is given, add nul bytes to fill to size bytes.
>>> long2raw(0x1219, BIG_ENDIAN)
'\x12\x19'
>>> long2raw(0x1219, BIG_ENDIAN, 4) # 32 bits
'\x00\x00\x12\x19'
>>> long2raw(0x1219, LITTLE_ENDIAN, 4) # 32 bits
'\x19\x12\x00\x00'
"""
assert (not size and 0 < value) or (0 <= value)
assert endian in (LITTLE_ENDIAN, BIG_ENDIAN)
text = []
while (value != 0 or text == ""):
byte = value % 256
text.append( chr(byte) )
value >>= 8
if size:
need = max(size - len(text), 0)
else:
need = 0
if need:
if endian is BIG_ENDIAN:
text = chain(repeat("\0", need), reversed(text))
else:
text = chain(text, repeat("\0", need))
else:
if endian is BIG_ENDIAN:
text = reversed(text)
return "".join(text)
def long2bin(size, value, endian, classic_mode=False):
"""
Convert a number into bits (in a string):
- size: size in bits of the number
- value: positive (or nul) number
- endian: BIG_ENDIAN (most important bit first)
or LITTLE_ENDIAN (least important bit first)
- classic_mode (default: False): reverse each packet of 8 bits
>>> long2bin(16, 1+4 + (1+8)*256, BIG_ENDIAN)
'10100000 10010000'
>>> long2bin(16, 1+4 + (1+8)*256, BIG_ENDIAN, True)
'00000101 00001001'
>>> long2bin(16, 1+4 + (1+8)*256, LITTLE_ENDIAN)
'00001001 00000101'
>>> long2bin(16, 1+4 + (1+8)*256, LITTLE_ENDIAN, True)
'10010000 10100000'
"""
text = ""
assert endian in (LITTLE_ENDIAN, BIG_ENDIAN)
assert 0 <= value
for index in xrange(size):
if (value & 1) == 1:
text += "1"
else:
text += "0"
value >>= 1
if endian is LITTLE_ENDIAN:
text = text[::-1]
result = ""
while len(text) != 0:
if len(result) != 0:
result += " "
if classic_mode:
result += text[7::-1]
else:
result += text[:8]
text = text[8:]
return result
def str2bin(value, classic_mode=True):
r"""
Convert binary string to binary numbers.
If classic_mode is true (default value), reverse bits.
>>> str2bin("\x03\xFF")
'00000011 11111111'
>>> str2bin("\x03\xFF", False)
'11000000 11111111'
"""
text = ""
for character in value:
if text != "":
text += " "
byte = ord(character)
text += byte2bin(byte, classic_mode)
return text
def _createStructFormat():
"""
Create a dictionnary (endian, size_byte) => struct format used
by str2long() to convert raw data to positive integer.
"""
format = {
BIG_ENDIAN: {},
LITTLE_ENDIAN: {},
}
for struct_format in "BHILQ":
try:
size = calcsize(struct_format)
format[BIG_ENDIAN][size] = '>%s' % struct_format
format[LITTLE_ENDIAN][size] = '<%s' % struct_format
except struct_error:
pass
return format
_struct_format = _createStructFormat()
def str2long(data, endian):
r"""
Convert a raw data (type 'str') into a long integer.
>>> chr(str2long('*', BIG_ENDIAN))
'*'
>>> str2long("\x00\x01\x02\x03", BIG_ENDIAN) == 0x10203
True
>>> str2long("\x2a\x10", LITTLE_ENDIAN) == 0x102a
True
>>> str2long("\xff\x14\x2a\x10", BIG_ENDIAN) == 0xff142a10
True
>>> str2long("\x00\x01\x02\x03", LITTLE_ENDIAN) == 0x3020100
True
>>> str2long("\xff\x14\x2a\x10\xab\x00\xd9\x0e", BIG_ENDIAN) == 0xff142a10ab00d90e
True
>>> str2long("\xff\xff\xff\xff\xff\xff\xff\xff", BIG_ENDIAN) == (2**64-1)
True
"""
assert 1 <= len(data) <= 32 # arbitrary limit: 256 bits
try:
return unpack(_struct_format[endian][len(data)], data)[0]
except KeyError:
pass
assert endian in (BIG_ENDIAN, LITTLE_ENDIAN)
shift = 0
value = 0
if endian is BIG_ENDIAN:
data = reversed(data)
for character in data:
byte = ord(character)
value += (byte << shift)
shift += 8
return value
-43
View File
@@ -1,43 +0,0 @@
from optparse import OptionGroup
from hachoir_core.log import log
from hachoir_core.i18n import _, getTerminalCharset
from hachoir_core.tools import makePrintable
import hachoir_core.config as config
def getHachoirOptions(parser):
"""
Create an option group (type optparse.OptionGroup) of Hachoir
library options.
"""
def setLogFilename(*args):
log.setFilename(args[2])
common = OptionGroup(parser, _("Hachoir library"), \
"Configure Hachoir library")
common.add_option("--verbose", help=_("Verbose mode"),
default=False, action="store_true")
common.add_option("--log", help=_("Write log in a file"),
type="string", action="callback", callback=setLogFilename)
common.add_option("--quiet", help=_("Quiet mode (don't display warning)"),
default=False, action="store_true")
common.add_option("--debug", help=_("Debug mode"),
default=False, action="store_true")
return common
def configureHachoir(option):
# Configure Hachoir using "option" (value from optparse)
if option.quiet:
config.quiet = True
if option.verbose:
config.verbose = True
if option.debug:
config.debug = True
def unicodeFilename(filename, charset=None):
if not charset:
charset = getTerminalCharset()
try:
return unicode(filename, charset)
except UnicodeDecodeError:
return makePrintable(filename, charset, to_unicode=True)
-185
View File
@@ -1,185 +0,0 @@
"""
Compatibility constants and functions. This module works on Python 1.5 to 2.5.
This module provides:
- True and False constants ;
- any() and all() function ;
- has_yield and has_slice values ;
- isinstance() with Python 2.3 behaviour ;
- reversed() and sorted() function.
True and False constants
========================
Truth constants: True is yes (one) and False is no (zero).
>>> int(True), int(False) # int value
(1, 0)
>>> int(False | True) # and binary operator
1
>>> int(True & False) # or binary operator
0
>>> int(not(True) == False) # not binary operator
1
Warning: on Python smaller than 2.3, True and False are aliases to
number 1 and 0. So "print True" will displays 1 and not True.
any() function
==============
any() returns True if at least one items is True, or False otherwise.
>>> any([False, True])
True
>>> any([True, True])
True
>>> any([False, False])
False
all() function
==============
all() returns True if all items are True, or False otherwise.
This function is just apply binary and operator (&) on all values.
>>> all([True, True])
True
>>> all([False, True])
False
>>> all([False, False])
False
has_yield boolean
=================
has_yield: boolean which indicatese if the interpreter supports yield keyword.
yield keyworkd is available since Python 2.0.
has_yield boolean
=================
has_slice: boolean which indicates if the interpreter supports slices with step
argument or not. slice with step is available since Python 2.3.
reversed() and sorted() function
================================
reversed() and sorted() function has been introduced in Python 2.4.
It's should returns a generator, but this module it may be a list.
>>> data = list("cab")
>>> list(sorted(data))
['a', 'b', 'c']
>>> list(reversed("abc"))
['c', 'b', 'a']
"""
import copy
import operator
# --- True and False constants from Python 2.0 ---
# --- Warning: for Python < 2.3, they are aliases for 1 and 0 ---
try:
True = True
False = False
except NameError:
True = 1
False = 0
# --- any() from Python 2.5 ---
try:
from __builtin__ import any
except ImportError:
def any(items):
for item in items:
if item:
return True
return False
# ---all() from Python 2.5 ---
try:
from __builtin__ import all
except ImportError:
def all(items):
return reduce(operator.__and__, items)
# --- test if interpreter supports yield keyword ---
try:
eval(compile("""
from __future__ import generators
def gen():
yield 1
yield 2
if list(gen()) != [1, 2]:
raise KeyError("42")
""", "<string>", "exec"))
except (KeyError, SyntaxError):
has_yield = False
else:
has_yield = True
# --- test if interpreter supports slices (with step argument) ---
try:
has_slice = eval('"abc"[::-1] == "cba"')
except (TypeError, SyntaxError):
has_slice = False
# --- isinstance with isinstance Python 2.3 behaviour (arg 2 is a type) ---
try:
if isinstance(1, int):
from __builtin__ import isinstance
except TypeError:
print "Redef isinstance"
def isinstance20(a, typea):
if type(typea) != type(type):
raise TypeError("TypeError: isinstance() arg 2 must be a class, type, or tuple of classes and types")
return type(typea) != typea
isinstance = isinstance20
# --- reversed() from Python 2.4 ---
try:
from __builtin__ import reversed
except ImportError:
# if hasYield() == "ok":
# code = """
#def reversed(data):
# for index in xrange(len(data)-1, -1, -1):
# yield data[index];
#reversed"""
# reversed = eval(compile(code, "<string>", "exec"))
if has_slice:
def reversed(data):
if not isinstance(data, list):
data = list(data)
return data[::-1]
else:
def reversed(data):
if not isinstance(data, list):
data = list(data)
reversed_data = []
for index in xrange(len(data)-1, -1, -1):
reversed_data.append(data[index])
return reversed_data
# --- sorted() from Python 2.4 ---
try:
from __builtin__ import sorted
except ImportError:
def sorted(data):
sorted_data = copy.copy(data)
sorted_data.sort()
return sorted
__all__ = ("True", "False",
"any", "all", "has_yield", "has_slice",
"isinstance", "reversed", "sorted")
-29
View File
@@ -1,29 +0,0 @@
"""
Configuration of Hachoir
"""
import os
# UI: display options
max_string_length = 40 # Max. length in characters of GenericString.display
max_byte_length = 14 # Max. length in bytes of RawBytes.display
max_bit_length = 256 # Max. length in bits of RawBits.display
unicode_stdout = True # Replace stdout and stderr with Unicode compatible objects
# Disable it for readline or ipython
# Global options
debug = False # Display many informations usefull to debug
verbose = False # Display more informations
quiet = False # Don't display warnings
# Use internationalization and localization (gettext)?
if os.name == "nt":
# TODO: Remove this hack and make i18n works on Windows :-)
use_i18n = False
else:
use_i18n = True
# Parser global options
autofix = True # Enable Autofix? see hachoir_core.field.GenericFieldSet
check_padding_pattern = True # Check padding fields pattern?
-183
View File
@@ -1,183 +0,0 @@
"""
Dictionnary classes which store values order.
"""
from hachoir_core.error import HachoirError
from hachoir_core.i18n import _
class UniqKeyError(HachoirError):
"""
Error raised when a value is set whereas the key already exist in a
dictionnary.
"""
pass
class Dict(object):
"""
This class works like classic Python dict() but has an important method:
__iter__() which allow to iterate into the dictionnary _values_ (and not
keys like Python's dict does).
"""
def __init__(self, values=None):
self._index = {} # key => index
self._key_list = [] # index => key
self._value_list = [] # index => value
if values:
for key, value in values:
self.append(key,value)
def _getValues(self):
return self._value_list
values = property(_getValues)
def index(self, key):
"""
Search a value by its key and returns its index
Returns None if the key doesn't exist.
>>> d=Dict( (("two", "deux"), ("one", "un")) )
>>> d.index("two")
0
>>> d.index("one")
1
>>> d.index("three") is None
True
"""
return self._index.get(key)
def __getitem__(self, key):
"""
Get item with specified key.
To get a value by it's index, use mydict.values[index]
>>> d=Dict( (("two", "deux"), ("one", "un")) )
>>> d["one"]
'un'
"""
return self._value_list[self._index[key]]
def __setitem__(self, key, value):
self._value_list[self._index[key]] = value
def append(self, key, value):
"""
Append new value
"""
if key in self._index:
raise UniqKeyError(_("Key '%s' already exists") % key)
self._index[key] = len(self._value_list)
self._key_list.append(key)
self._value_list.append(value)
def __len__(self):
return len(self._value_list)
def __contains__(self, key):
return key in self._index
def __iter__(self):
return iter(self._value_list)
def iteritems(self):
"""
Create a generator to iterate on: (key, value).
>>> d=Dict( (("two", "deux"), ("one", "un")) )
>>> for key, value in d.iteritems():
... print "%r: %r" % (key, value)
...
'two': 'deux'
'one': 'un'
"""
for index in xrange(len(self)):
yield (self._key_list[index], self._value_list[index])
def itervalues(self):
"""
Create an iterator on values
"""
return iter(self._value_list)
def iterkeys(self):
"""
Create an iterator on keys
"""
return iter(self._key_list)
def replace(self, oldkey, newkey, new_value):
"""
Replace an existing value with another one
>>> d=Dict( (("two", "deux"), ("one", "un")) )
>>> d.replace("one", "three", 3)
>>> d
{'two': 'deux', 'three': 3}
You can also use the classic form:
>>> d['three'] = 4
>>> d
{'two': 'deux', 'three': 4}
"""
index = self._index[oldkey]
self._value_list[index] = new_value
if oldkey != newkey:
del self._index[oldkey]
self._index[newkey] = index
self._key_list[index] = newkey
def __delitem__(self, index):
"""
Delete item at position index. May raise IndexError.
>>> d=Dict( ((6, 'six'), (9, 'neuf'), (4, 'quatre')) )
>>> del d[1]
>>> d
{6: 'six', 4: 'quatre'}
"""
if index < 0:
index += len(self._value_list)
if not (0 <= index < len(self._value_list)):
raise IndexError(_("list assignment index out of range (%s/%s)")
% (index, len(self._value_list)))
del self._value_list[index]
del self._key_list[index]
# First loop which may alter self._index
for key, item_index in self._index.iteritems():
if item_index == index:
del self._index[key]
break
# Second loop update indexes
for key, item_index in self._index.iteritems():
if index < item_index:
self._index[key] -= 1
def insert(self, index, key, value):
"""
Insert an item at specified position index.
>>> d=Dict( ((6, 'six'), (9, 'neuf'), (4, 'quatre')) )
>>> d.insert(1, '40', 'quarante')
>>> d
{6: 'six', '40': 'quarante', 9: 'neuf', 4: 'quatre'}
"""
if key in self:
raise UniqKeyError(_("Insert error: key '%s' ready exists") % key)
_index = index
if index < 0:
index += len(self._value_list)
if not(0 <= index <= len(self._value_list)):
raise IndexError(_("Insert error: index '%s' is invalid") % _index)
for item_key, item_index in self._index.iteritems():
if item_index >= index:
self._index[item_key] += 1
self._index[key] = index
self._key_list.insert(index, key)
self._value_list.insert(index, value)
def __repr__(self):
items = ( "%r: %r" % (key, value) for key, value in self.iteritems() )
return "{%s}" % ", ".join(items)
-15
View File
@@ -1,15 +0,0 @@
"""
Constant values about endian.
"""
from hachoir_core.i18n import _
BIG_ENDIAN = "ABCD"
LITTLE_ENDIAN = "DCBA"
NETWORK_ENDIAN = BIG_ENDIAN
endian_name = {
BIG_ENDIAN: _("Big endian"),
LITTLE_ENDIAN: _("Little endian"),
}
-45
View File
@@ -1,45 +0,0 @@
"""
Functions to display an error (error, warning or information) message.
"""
from hachoir_core.log import log
from hachoir_core.tools import makePrintable
import sys, traceback
def getBacktrace(empty="Empty backtrace."):
"""
Try to get backtrace as string.
Returns "Error while trying to get backtrace" on failure.
"""
try:
info = sys.exc_info()
trace = traceback.format_exception(*info)
sys.exc_clear()
if trace[0] != "None\n":
return "".join(trace)
except:
# No i18n here (imagine if i18n function calls error...)
return "Error while trying to get backtrace"
return empty
class HachoirError(Exception):
"""
Parent of all errors in Hachoir library
"""
def __init__(self, message):
message_bytes = makePrintable(message, "ASCII")
Exception.__init__(self, message_bytes)
self.text = message
def __unicode__(self):
return self.text
# Error classes which may be raised by Hachoir core
# FIXME: Add EnvironmentError (IOError or OSError) and AssertionError?
# FIXME: Remove ArithmeticError and RuntimeError?
HACHOIR_ERRORS = (HachoirError, LookupError, NameError, AttributeError,
TypeError, ValueError, ArithmeticError, RuntimeError)
info = log.info
warning = log.warning
error = log.error
-26
View File
@@ -1,26 +0,0 @@
class EventHandler(object):
"""
Class to connect events to event handlers.
"""
def __init__(self):
self.handlers = {}
def connect(self, event_name, handler):
"""
Connect an event handler to an event. Append it to handlers list.
"""
try:
self.handlers[event_name].append(handler)
except KeyError:
self.handlers[event_name] = [handler]
def raiseEvent(self, event_name, *args):
"""
Raiser an event: call each handler for this event_name.
"""
if event_name not in self.handlers:
return
for handler in self.handlers[event_name]:
handler(*args)
-59
View File
@@ -1,59 +0,0 @@
# Field classes
from hachoir_core.field.field import Field, FieldError, MissingField, joinPath
from hachoir_core.field.bit_field import Bit, Bits, RawBits
from hachoir_core.field.byte_field import Bytes, RawBytes
from hachoir_core.field.sub_file import SubFile, CompressedField
from hachoir_core.field.character import Character
from hachoir_core.field.integer import (
Int8, Int16, Int24, Int32, Int64,
UInt8, UInt16, UInt24, UInt32, UInt64,
GenericInteger)
from hachoir_core.field.enum import Enum
from hachoir_core.field.string_field import (GenericString,
String, CString, UnixLine,
PascalString8, PascalString16, PascalString32)
from hachoir_core.field.padding import (PaddingBits, PaddingBytes,
NullBits, NullBytes)
# Functions
from hachoir_core.field.helper import (isString, isInteger,
createPaddingField, createNullField, createRawField,
writeIntoFile, createOrphanField)
# FieldSet classes
from hachoir_core.field.fake_array import FakeArray
from hachoir_core.field.basic_field_set import (BasicFieldSet,
ParserError, MatchError)
from hachoir_core.field.generic_field_set import GenericFieldSet
from hachoir_core.field.seekable_field_set import SeekableFieldSet, RootSeekableFieldSet
from hachoir_core.field.field_set import FieldSet
from hachoir_core.field.static_field_set import StaticFieldSet
from hachoir_core.field.parser import Parser
from hachoir_core.field.vector import GenericVector, UserVector
# Complex types
from hachoir_core.field.float import Float32, Float64, Float80
from hachoir_core.field.timestamp import (GenericTimestamp,
TimestampUnix32, TimestampUnix64, TimestampMac32, TimestampUUID60, TimestampWin64,
DateTimeMSDOS32, TimeDateMSDOS32, TimedeltaWin64)
# Special Field classes
from hachoir_core.field.link import Link, Fragment
available_types = (
Bit, Bits, RawBits,
Bytes, RawBytes,
SubFile,
Character,
Int8, Int16, Int24, Int32, Int64,
UInt8, UInt16, UInt24, UInt32, UInt64,
String, CString, UnixLine,
PascalString8, PascalString16, PascalString32,
Float32, Float64,
PaddingBits, PaddingBytes,
NullBits, NullBytes,
TimestampUnix32, TimestampMac32, TimestampWin64,
DateTimeMSDOS32, TimeDateMSDOS32,
# GenericInteger, GenericString,
)
-147
View File
@@ -1,147 +0,0 @@
from hachoir_core.field import Field, FieldError
from hachoir_core.stream import InputStream
from hachoir_core.endian import BIG_ENDIAN, LITTLE_ENDIAN
from hachoir_core.event_handler import EventHandler
class ParserError(FieldError):
"""
Error raised by a field set.
@see: L{FieldError}
"""
pass
class MatchError(FieldError):
"""
Error raised by a field set when the stream content doesn't
match to file format.
@see: L{FieldError}
"""
pass
class BasicFieldSet(Field):
_event_handler = None
is_field_set = True
endian = None
def __init__(self, parent, name, stream, description, size):
# Sanity checks (preconditions)
assert not parent or issubclass(parent.__class__, BasicFieldSet)
assert issubclass(stream.__class__, InputStream)
# Set field set size
if size is None and self.static_size:
assert isinstance(self.static_size, (int, long))
size = self.static_size
# Set Field attributes
self._parent = parent
self._name = name
self._size = size
self._description = description
self.stream = stream
self._field_array_count = {}
# Set endian
if not self.endian:
assert parent and parent.endian
self.endian = parent.endian
if parent:
# This field set is one of the root leafs
self._address = parent.nextFieldAddress()
self.root = parent.root
assert id(self.stream) == id(parent.stream)
else:
# This field set is the root
self._address = 0
self.root = self
self._global_event_handler = None
# Sanity checks (post-conditions)
assert self.endian in (BIG_ENDIAN, LITTLE_ENDIAN)
if (self._size is not None) and (self._size <= 0):
raise ParserError("Invalid parser '%s' size: %s" % (self.path, self._size))
def reset(self):
self._field_array_count = {}
def createValue(self):
return None
def connectEvent(self, event_name, handler, local=True):
assert event_name in (
# Callback prototype: def f(field)
# Called when new value is already set
"field-value-changed",
# Callback prototype: def f(field)
# Called when field size is already set
"field-resized",
# A new field has been inserted in the field set
# Callback prototype: def f(index, new_field)
"field-inserted",
# Callback prototype: def f(old_field, new_field)
# Called when new field is already in field set
"field-replaced",
# Callback prototype: def f(field, new_value)
# Called to ask to set new value
"set-field-value"
), "Event name %r is invalid" % event_name
if local:
if self._event_handler is None:
self._event_handler = EventHandler()
self._event_handler.connect(event_name, handler)
else:
if self.root._global_event_handler is None:
self.root._global_event_handler = EventHandler()
self.root._global_event_handler.connect(event_name, handler)
def raiseEvent(self, event_name, *args):
# Transfer event to local listeners
if self._event_handler is not None:
self._event_handler.raiseEvent(event_name, *args)
# Transfer event to global listeners
if self.root._global_event_handler is not None:
self.root._global_event_handler.raiseEvent(event_name, *args)
def setUniqueFieldName(self, field):
key = field._name[:-2]
try:
self._field_array_count[key] += 1
except KeyError:
self._field_array_count[key] = 0
field._name = key + "[%u]" % self._field_array_count[key]
def readFirstFields(self, number):
"""
Read first number fields if they are not read yet.
Returns number of new added fields.
"""
number = number - self.current_length
if 0 < number:
return self.readMoreFields(number)
else:
return 0
def createFields(self):
raise NotImplementedError()
def __iter__(self):
raise NotImplementedError()
def __len__(self):
raise NotImplementedError()
def getField(self, key, const=True):
raise NotImplementedError()
def nextFieldAddress(self):
raise NotImplementedError()
def getFieldIndex(self, field):
raise NotImplementedError()
def readMoreFields(self, number):
raise NotImplementedError()
-68
View File
@@ -1,68 +0,0 @@
"""
Bit sized classes:
- Bit: Single bit, value is False or True ;
- Bits: Integer with a size in bits ;
- RawBits: unknown content with a size in bits.
"""
from hachoir_core.field import Field
from hachoir_core.i18n import _
from hachoir_core import config
class RawBits(Field):
"""
Unknown content with a size in bits.
"""
static_size = staticmethod(lambda *args, **kw: args[1])
def __init__(self, parent, name, size, description=None):
"""
Constructor: see L{Field.__init__} for parameter description
"""
Field.__init__(self, parent, name, size, description)
def hasValue(self):
return True
def createValue(self):
return self._parent.stream.readBits(
self.absolute_address, self._size, self._parent.endian)
def createDisplay(self):
if self._size < config.max_bit_length:
return unicode(self.value)
else:
return _("<%s size=%u>" %
(self.__class__.__name__, self._size))
createRawDisplay = createDisplay
class Bits(RawBits):
"""
Positive integer with a size in bits
@see: L{Bit}
@see: L{RawBits}
"""
pass
class Bit(RawBits):
"""
Single bit: value can be False or True, and size is exactly one bit.
@see: L{Bits}
"""
static_size = 1
def __init__(self, parent, name, description=None):
"""
Constructor: see L{Field.__init__} for parameter description
"""
RawBits.__init__(self, parent, name, 1, description=description)
def createValue(self):
return 1 == self._parent.stream.readBits(
self.absolute_address, 1, self._parent.endian)
def createRawDisplay(self):
return unicode(int(self.value))
-73
View File
@@ -1,73 +0,0 @@
"""
Very basic field: raw content with a size in byte. Use this class for
unknown content.
"""
from hachoir_core.field import Field, FieldError
from hachoir_core.tools import makePrintable
from hachoir_core.bits import str2hex
from hachoir_core import config
MAX_LENGTH = (2**64)
class RawBytes(Field):
"""
Byte vector of unknown content
@see: L{Bytes}
"""
static_size = staticmethod(lambda *args, **kw: args[1]*8)
def __init__(self, parent, name, length, description="Raw data"):
assert issubclass(parent.__class__, Field)
if not(0 < length <= MAX_LENGTH):
raise FieldError("Invalid RawBytes length (%s)!" % length)
Field.__init__(self, parent, name, length*8, description)
self._display = None
def _createDisplay(self, human):
max_bytes = config.max_byte_length
if type(self._getValue) is type(lambda: None):
display = self.value[:max_bytes]
else:
if self._display is None:
address = self.absolute_address
length = min(self._size / 8, max_bytes)
self._display = self._parent.stream.readBytes(address, length)
display = self._display
truncated = (8 * len(display) < self._size)
if human:
if truncated:
display += "(...)"
return makePrintable(display, "latin-1", quote='"', to_unicode=True)
else:
display = str2hex(display, format=r"\x%02x")
if truncated:
return '"%s(...)"' % display
else:
return '"%s"' % display
def createDisplay(self):
return self._createDisplay(True)
def createRawDisplay(self):
return self._createDisplay(False)
def hasValue(self):
return True
def createValue(self):
assert (self._size % 8) == 0
if self._display:
self._display = None
return self._parent.stream.readBytes(
self.absolute_address, self._size / 8)
class Bytes(RawBytes):
"""
Byte vector: can be used for magic number or GUID/UUID for example.
@see: L{RawBytes}
"""
pass
-27
View File
@@ -1,27 +0,0 @@
"""
Character field class: a 8-bit character
"""
from hachoir_core.field import Bits
from hachoir_core.endian import BIG_ENDIAN
from hachoir_core.tools import makePrintable
class Character(Bits):
"""
A 8-bit character using ASCII charset for display attribute.
"""
static_size = 8
def __init__(self, parent, name, description=None):
Bits.__init__(self, parent, name, 8, description=description)
def createValue(self):
return chr(self._parent.stream.readBits(
self.absolute_address, 8, BIG_ENDIAN))
def createRawDisplay(self):
return unicode(Bits.createValue(self))
def createDisplay(self):
return makePrintable(self.value, "ASCII", quote="'", to_unicode=True)
-26
View File
@@ -1,26 +0,0 @@
def Enum(field, enum, key_func=None):
"""
Enum is an adapter to another field: it will just change its display
attribute. It uses a dictionary to associate a value to another.
key_func is an optional function with prototype "def func(key)->key"
which is called to transform key.
"""
display = field.createDisplay
if key_func:
def createDisplay():
try:
key = key_func(field.value)
return enum[key]
except LookupError:
return display()
else:
def createDisplay():
try:
return enum[field.value]
except LookupError:
return display()
field.createDisplay = createDisplay
field.getEnum = lambda: enum
return field
-81
View File
@@ -1,81 +0,0 @@
import itertools
from hachoir_core.field import MissingField
class FakeArray:
"""
Simulate an array for GenericFieldSet.array(): fielset.array("item")[0] is
equivalent to fielset.array("item[0]").
It's possible to iterate over the items using::
for element in fieldset.array("item"):
...
And to get array size using len(fieldset.array("item")).
"""
def __init__(self, fieldset, name):
pos = name.rfind("/")
if pos != -1:
self.fieldset = fieldset[name[:pos]]
self.name = name[pos+1:]
else:
self.fieldset = fieldset
self.name = name
self._format = "%s[%%u]" % self.name
self._cache = {}
self._known_size = False
self._max_index = -1
def __nonzero__(self):
"Is the array empty or not?"
if self._cache:
return True
else:
return (0 in self)
def __len__(self):
"Number of fields in the array"
total = self._max_index+1
if not self._known_size:
for index in itertools.count(total):
try:
field = self[index]
total += 1
except MissingField:
break
return total
def __contains__(self, index):
try:
field = self[index]
return True
except MissingField:
return False
def __getitem__(self, index):
"""
Get a field of the array. Returns a field, or raise MissingField
exception if the field doesn't exist.
"""
try:
value = self._cache[index]
except KeyError:
try:
value = self.fieldset[self._format % index]
except MissingField:
self._known_size = True
raise
self._cache[index] = value
self._max_index = max(index, self._max_index)
return value
def __iter__(self):
"""
Iterate in the fields in their index order: field[0], field[1], ...
"""
for index in itertools.count(0):
try:
yield self[index]
except MissingField:
raise StopIteration()
-262
View File
@@ -1,262 +0,0 @@
"""
Parent of all (field) classes in Hachoir: Field.
"""
from hachoir_core.compatibility import reversed
from hachoir_core.stream import InputFieldStream
from hachoir_core.error import HachoirError, HACHOIR_ERRORS
from hachoir_core.log import Logger
from hachoir_core.i18n import _
from hachoir_core.tools import makePrintable
from weakref import ref as weakref_ref
class FieldError(HachoirError):
"""
Error raised by a L{Field}.
@see: L{HachoirError}
"""
pass
def joinPath(path, name):
if path != "/":
return "/".join((path, name))
else:
return "/%s" % name
class MissingField(KeyError, FieldError):
def __init__(self, field, key):
KeyError.__init__(self)
self.field = field
self.key = key
def __str__(self):
return 'Can\'t get field "%s" from %s' % (self.key, self.field.path)
def __unicode__(self):
return u'Can\'t get field "%s" from %s' % (self.key, self.field.path)
class Field(Logger):
# static size can have two differents value: None (no static size), an
# integer (number of bits), or a function which returns an integer.
#
# This function receives exactly the same arguments than the constructor
# except the first one (one). Example of function:
# static_size = staticmethod(lambda *args, **kw: args[1])
static_size = None
# Indicate if this field contains other fields (is a field set) or not
is_field_set = False
def __init__(self, parent, name, size=None, description=None):
"""
Set default class attributes, set right address if None address is
given.
@param parent: Parent field of this field
@type parent: L{Field}|None
@param name: Name of the field, have to be unique in parent. If it ends
with "[]", end will be replaced with "[new_id]" (eg. "raw[]"
becomes "raw[0]", next will be "raw[1]", and then "raw[2]", etc.)
@type name: str
@param size: Size of the field in bit (can be None, so it
will be computed later)
@type size: int|None
@param address: Address in bit relative to the parent absolute address
@type address: int|None
@param description: Optional string description
@type description: str|None
"""
assert issubclass(parent.__class__, Field)
assert (size is None) or (0 <= size)
self._parent = parent
if not name:
raise ValueError("empty field name")
self._name = name
self._address = parent.nextFieldAddress()
self._size = size
self._description = description
def _logger(self):
return self.path
def createDescription(self):
return ""
def _getDescription(self):
if self._description is None:
try:
self._description = self.createDescription()
if isinstance(self._description, str):
self._description = makePrintable(
self._description, "ISO-8859-1", to_unicode=True)
except HACHOIR_ERRORS, err:
self.error("Error getting description: " + unicode(err))
self._description = ""
return self._description
description = property(_getDescription,
doc="Description of the field (string)")
def __str__(self):
return self.display
def __unicode__(self):
return self.display
def __repr__(self):
return "<%s path=%r, address=%s, size=%s>" % (
self.__class__.__name__, self.path, self._address, self._size)
def hasValue(self):
return self._getValue() is not None
def createValue(self):
raise NotImplementedError()
def _getValue(self):
try:
value = self.createValue()
except HACHOIR_ERRORS, err:
self.error(_("Unable to create value: %s") % unicode(err))
value = None
self._getValue = lambda: value
return value
value = property(lambda self: self._getValue(), doc="Value of field")
def _getParent(self):
return self._parent
parent = property(_getParent, doc="Parent of this field")
def createDisplay(self):
return unicode(self.value)
def _getDisplay(self):
if not hasattr(self, "_Field__display"):
try:
self.__display = self.createDisplay()
except HACHOIR_ERRORS, err:
self.error("Unable to create display: %s" % err)
self.__display = u""
return self.__display
display = property(lambda self: self._getDisplay(),
doc="Short (unicode) string which represents field content")
def createRawDisplay(self):
value = self.value
if isinstance(value, str):
return makePrintable(value, "ASCII", to_unicode=True)
else:
return unicode(value)
def _getRawDisplay(self):
if not hasattr(self, "_Field__raw_display"):
try:
self.__raw_display = self.createRawDisplay()
except HACHOIR_ERRORS, err:
self.error("Unable to create raw display: %s" % err)
self.__raw_display = u""
return self.__raw_display
raw_display = property(lambda self: self._getRawDisplay(),
doc="(Unicode) string which represents raw field content")
def _getName(self):
return self._name
name = property(_getName,
doc="Field name (unique in its parent field set list)")
def _getIndex(self):
if not self._parent:
return None
return self._parent.getFieldIndex(self)
index = property(_getIndex)
def _getPath(self):
if not self._parent:
return '/'
names = []
field = self
while field is not None:
names.append(field._name)
field = field._parent
names[-1] = ''
return '/'.join(reversed(names))
path = property(_getPath,
doc="Full path of the field starting at root field")
def _getAddress(self):
return self._address
address = property(_getAddress,
doc="Relative address in bit to parent address")
def _getAbsoluteAddress(self):
address = self._address
current = self._parent
while current:
address += current._address
current = current._parent
return address
absolute_address = property(_getAbsoluteAddress,
doc="Absolute address (from stream beginning) in bit")
def _getSize(self):
return self._size
size = property(_getSize, doc="Content size in bit")
def _getField(self, name, const):
if name.strip("."):
return None
field = self
for index in xrange(1, len(name)):
field = field._parent
if field is None:
break
return field
def getField(self, key, const=True):
if key:
if key[0] == "/":
if self._parent:
current = self._parent.root
else:
current = self
if len(key) == 1:
return current
key = key[1:]
else:
current = self
for part in key.split("/"):
field = current._getField(part, const)
if field is None:
raise MissingField(current, part)
current = field
return current
raise KeyError("Key must not be an empty string!")
def __getitem__(self, key):
return self.getField(key, False)
def __contains__(self, key):
try:
return self.getField(key, False) is not None
except FieldError:
return False
def _createInputStream(self, **args):
assert self._parent
return InputFieldStream(self, **args)
def getSubIStream(self):
if hasattr(self, "_sub_istream"):
stream = self._sub_istream()
else:
stream = None
if stream is None:
stream = self._createInputStream()
self._sub_istream = weakref_ref(stream)
return stream
def setSubIStream(self, createInputStream):
cis = self._createInputStream
self._createInputStream = lambda **args: createInputStream(cis, **args)
def __nonzero__(self):
"""
Method called by code like "if field: (...)".
Always returns True
"""
return True
def getFieldType(self):
return self.__class__.__name__
-7
View File
@@ -1,7 +0,0 @@
from hachoir_core.field import BasicFieldSet, GenericFieldSet
class FieldSet(GenericFieldSet):
def __init__(self, parent, name, *args, **kw):
assert issubclass(parent.__class__, BasicFieldSet)
GenericFieldSet.__init__(self, parent, name, parent.stream, *args, **kw)
-99
View File
@@ -1,99 +0,0 @@
from hachoir_core.field import Bit, Bits, FieldSet
from hachoir_core.endian import BIG_ENDIAN, LITTLE_ENDIAN
import struct
# Make sure that we use right struct types
assert struct.calcsize("f") == 4
assert struct.calcsize("d") == 8
assert struct.unpack("<d", "\x1f\x85\xebQ\xb8\x1e\t@")[0] == 3.14
assert struct.unpack(">d", "\xc0\0\0\0\0\0\0\0")[0] == -2.0
class FloatMantissa(Bits):
def createValue(self):
value = Bits.createValue(self)
return 1 + float(value) / (2 ** self.size)
def createRawDisplay(self):
return unicode(Bits.createValue(self))
class FloatExponent(Bits):
def __init__(self, parent, name, size):
Bits.__init__(self, parent, name, size)
self.bias = 2 ** (size-1) - 1
def createValue(self):
return Bits.createValue(self) - self.bias
def createRawDisplay(self):
return unicode(self.value + self.bias)
def floatFactory(name, format, mantissa_bits, exponent_bits, doc):
size = 1 + mantissa_bits + exponent_bits
class Float(FieldSet):
static_size = size
__doc__ = doc
def __init__(self, parent, name, description=None):
assert parent.endian in (BIG_ENDIAN, LITTLE_ENDIAN)
FieldSet.__init__(self, parent, name, description, size)
if format:
if self._parent.endian == BIG_ENDIAN:
self.struct_format = ">"+format
else:
self.struct_format = "<"+format
else:
self.struct_format = None
def createValue(self):
"""
Create float value: use struct.unpack() when it's possible
(32 and 64-bit float) or compute it with :
mantissa * (2.0 ** exponent)
This computation may raise an OverflowError.
"""
if self.struct_format:
raw = self._parent.stream.readBytes(
self.absolute_address, self._size//8)
try:
return struct.unpack(self.struct_format, raw)[0]
except struct.error, err:
raise ValueError("[%s] conversion error: %s" %
(self.__class__.__name__, err))
else:
try:
value = self["mantissa"].value * (2.0 ** float(self["exponent"].value))
if self["negative"].value:
return -(value)
else:
return value
except OverflowError:
raise ValueError("[%s] floating point overflow" %
self.__class__.__name__)
def createFields(self):
yield Bit(self, "negative")
yield FloatExponent(self, "exponent", exponent_bits)
if 64 <= mantissa_bits:
yield Bit(self, "one")
yield FloatMantissa(self, "mantissa", mantissa_bits-1)
else:
yield FloatMantissa(self, "mantissa", mantissa_bits)
cls = Float
cls.__name__ = name
return cls
# 32-bit float (standard: IEEE 754/854)
Float32 = floatFactory("Float32", "f", 23, 8,
"Floating point number: format IEEE 754 int 32 bit")
# 64-bit float (standard: IEEE 754/854)
Float64 = floatFactory("Float64", "d", 52, 11,
"Floating point number: format IEEE 754 in 64 bit")
# 80-bit float (standard: IEEE 754/854)
Float80 = floatFactory("Float80", None, 64, 15,
"Floating point number: format IEEE 754 in 80 bit")
@@ -1,532 +0,0 @@
from hachoir_core.field import (MissingField, BasicFieldSet, Field, ParserError,
createRawField, createNullField, createPaddingField, FakeArray)
from hachoir_core.dict import Dict, UniqKeyError
from hachoir_core.error import HACHOIR_ERRORS
from hachoir_core.tools import lowerBound
import hachoir_core.config as config
class GenericFieldSet(BasicFieldSet):
"""
Ordered list of fields. Use operator [] to access fields using their
name (field names are unique in a field set, but not in the whole
document).
Class attributes:
- endian: Bytes order (L{BIG_ENDIAN} or L{LITTLE_ENDIAN}). Optional if the
field set has a parent ;
- static_size: (optional) Size of FieldSet in bits. This attribute should
be used in parser of constant size.
Instance attributes/methods:
- _fields: Ordered dictionnary of all fields, may be incomplete
because feeded when a field is requested ;
- stream: Input stream used to feed fields' value
- root: The root of all field sets ;
- __len__(): Number of fields, may need to create field set ;
- __getitem__(): Get an field by it's name or it's path.
And attributes inherited from Field class:
- parent: Parent field (may be None if it's the root) ;
- name: Field name (unique in parent field set) ;
- value: The field set ;
- address: Field address (in bits) relative to parent ;
- description: A string describing the content (can be None) ;
- size: Size of field set in bits, may need to create field set.
Event handling:
- "connectEvent": Connect an handler to an event ;
- "raiseEvent": Raise an event.
To implement a new field set, you need to:
- create a class which inherite from FieldSet ;
- write createFields() method using lines like:
yield Class(self, "name", ...) ;
- and maybe set endian and static_size class attributes.
"""
_current_size = 0
def __init__(self, parent, name, stream, description=None, size=None):
"""
Constructor
@param parent: Parent field set, None for root parser
@param name: Name of the field, have to be unique in parent. If it ends
with "[]", end will be replaced with "[new_id]" (eg. "raw[]"
becomes "raw[0]", next will be "raw[1]", and then "raw[2]", etc.)
@type name: str
@param stream: Input stream from which data are read
@type stream: L{InputStream}
@param description: Optional string description
@type description: str|None
@param size: Size in bits. If it's None, size will be computed. You
can also set size with class attribute static_size
"""
BasicFieldSet.__init__(self, parent, name, stream, description, size)
self._fields = Dict()
self._field_generator = self.createFields()
self._array_cache = {}
self.__is_feeding = False
def array(self, key):
try:
return self._array_cache[key]
except KeyError:
array = FakeArray(self, key)
self._array_cache[key] = array
return self._array_cache[key]
def reset(self):
"""
Reset a field set:
* clear fields ;
* restart field generator ;
* set current size to zero ;
* clear field array count.
But keep: name, value, description and size.
"""
BasicFieldSet.reset(self)
self._fields = Dict()
self._field_generator = self.createFields()
self._current_size = 0
self._array_cache = {}
def __str__(self):
return '<%s path=%s, current_size=%s, current length=%s>' % \
(self.__class__.__name__, self.path, self._current_size, len(self._fields))
def __len__(self):
"""
Returns number of fields, may need to create all fields
if it's not done yet.
"""
if self._field_generator is not None:
self._feedAll()
return len(self._fields)
def _getCurrentLength(self):
return len(self._fields)
current_length = property(_getCurrentLength)
def _getSize(self):
if self._size is None:
self._feedAll()
return self._size
size = property(_getSize, doc="Size in bits, may create all fields to get size")
def _getCurrentSize(self):
assert not(self.done)
return self._current_size
current_size = property(_getCurrentSize)
eof = property(lambda self: self._checkSize(self._current_size + 1, True) < 0)
def _checkSize(self, size, strict):
field = self
while field._size is None:
if not field._parent:
assert self.stream.size is None
if not strict:
return None
if self.stream.sizeGe(size):
return 0
break
size += field._address
field = field._parent
return field._size - size
autofix = property(lambda self: self.root.autofix)
def _addField(self, field):
"""
Add a field to the field set:
* add it into _fields
* update _current_size
May raise a StopIteration() on error
"""
if not issubclass(field.__class__, Field):
raise ParserError("Field type (%s) is not a subclass of 'Field'!"
% field.__class__.__name__)
assert isinstance(field._name, str)
if field._name.endswith("[]"):
self.setUniqueFieldName(field)
if config.debug:
self.info("[+] DBG: _addField(%s)" % field.name)
# required for the msoffice parser
if field._address != self._current_size:
self.warning("Fix address of %s to %s (was %s)" %
(field.path, self._current_size, field._address))
field._address = self._current_size
ask_stop = False
# Compute field size and check that there is enough place for it
self.__is_feeding = True
try:
field_size = field.size
except HACHOIR_ERRORS, err:
if field.is_field_set and field.current_length and field.eof:
self.warning("Error when getting size of '%s': %s" % (field.name, err))
field._stopFeeding()
ask_stop = True
else:
self.warning("Error when getting size of '%s': delete it" % field.name)
self.__is_feeding = False
raise
self.__is_feeding = False
# No more place?
dsize = self._checkSize(field._address + field.size, False)
if (dsize is not None and dsize < 0) or (field.is_field_set and field.size <= 0):
if self.autofix and self._current_size:
self._fixFieldSize(field, field.size + dsize)
else:
raise ParserError("Field %s is too large!" % field.path)
self._current_size += field.size
try:
self._fields.append(field._name, field)
except UniqKeyError, err:
self.warning("Duplicate field name " + unicode(err))
field._name += "[]"
self.setUniqueFieldName(field)
self._fields.append(field._name, field)
if ask_stop:
raise StopIteration()
def _fixFieldSize(self, field, new_size):
if new_size > 0:
if field.is_field_set and 0 < field.size:
field._truncate(new_size)
return
# Don't add the field <=> delete item
if self._size is None:
self._size = self._current_size + new_size
self.warning("[Autofix] Delete '%s' (too large)" % field.path)
raise StopIteration()
def _getField(self, name, const):
field = Field._getField(self, name, const)
if field is None:
if name in self._fields:
field = self._fields[name]
elif self._field_generator is not None and not const:
field = self._feedUntil(name)
return field
def getField(self, key, const=True):
if isinstance(key, (int, long)):
if key < 0:
raise KeyError("Key must be positive!")
if not const:
self.readFirstFields(key+1)
if len(self._fields.values) <= key:
raise MissingField(self, key)
return self._fields.values[key]
return Field.getField(self, key, const)
def _truncate(self, size):
assert size > 0
if size < self._current_size:
self._size = size
while True:
field = self._fields.values[-1]
if field._address < size:
break
del self._fields[-1]
self._current_size = field._address
size -= field._address
if size < field._size:
if field.is_field_set:
field._truncate(size)
else:
del self._fields[-1]
field = createRawField(self, size, "raw[]")
self._fields.append(field._name, field)
self._current_size = self._size
else:
assert size < self._size or self._size is None
self._size = size
if self._size == self._current_size:
self._field_generator = None
def _deleteField(self, index):
field = self._fields.values[index]
size = field.size
self._current_size -= size
del self._fields[index]
return field
def _fixLastField(self):
"""
Try to fix last field when we know current field set size.
Returns new added field if any, or None.
"""
assert self._size is not None
# Stop parser
message = ["stop parser"]
self._field_generator = None
# If last field is too big, delete it
while self._size < self._current_size:
field = self._deleteField(len(self._fields)-1)
message.append("delete field %s" % field.path)
assert self._current_size <= self._size
# If field size current is smaller: add a raw field
size = self._size - self._current_size
if size:
field = createRawField(self, size, "raw[]")
message.append("add padding")
self._current_size += field.size
self._fields.append(field._name, field)
else:
field = None
message = ", ".join(message)
self.warning("[Autofix] Fix parser error: " + message)
assert self._current_size == self._size
return field
def _stopFeeding(self):
new_field = None
if self._size is None:
if self._parent:
self._size = self._current_size
elif self._size != self._current_size:
if self.autofix:
new_field = self._fixLastField()
else:
raise ParserError("Invalid parser \"%s\" size!" % self.path)
self._field_generator = None
return new_field
def _fixFeedError(self, exception):
"""
Try to fix a feeding error. Returns False if error can't be fixed,
otherwise returns new field if any, or None.
"""
if self._size is None or not self.autofix:
return False
self.warning(unicode(exception))
return self._fixLastField()
def _feedUntil(self, field_name):
"""
Return the field if it was found, None else
"""
if self.__is_feeding \
or (self._field_generator and self._field_generator.gi_running):
self.warning("Unable to get %s (and generator is already running)"
% field_name)
return None
try:
while True:
field = self._field_generator.next()
self._addField(field)
if field.name == field_name:
return field
except HACHOIR_ERRORS, err:
if self._fixFeedError(err) is False:
raise
except StopIteration:
self._stopFeeding()
return None
def readMoreFields(self, number):
"""
Read more number fields, or do nothing if parsing is done.
Returns number of new added fields.
"""
if self._field_generator is None:
return 0
oldlen = len(self._fields)
try:
for index in xrange(number):
self._addField( self._field_generator.next() )
except HACHOIR_ERRORS, err:
if self._fixFeedError(err) is False:
raise
except StopIteration:
self._stopFeeding()
return len(self._fields) - oldlen
def _feedAll(self):
if self._field_generator is None:
return
try:
while True:
field = self._field_generator.next()
self._addField(field)
except HACHOIR_ERRORS, err:
if self._fixFeedError(err) is False:
raise
except StopIteration:
self._stopFeeding()
def __iter__(self):
"""
Create a generator to iterate on each field, may create new
fields when needed
"""
try:
done = 0
while True:
if done == len(self._fields):
if self._field_generator is None:
break
self._addField( self._field_generator.next() )
for field in self._fields.values[done:]:
yield field
done += 1
except HACHOIR_ERRORS, err:
field = self._fixFeedError(err)
if isinstance(field, Field):
yield field
elif hasattr(field, '__iter__'):
for f in field:
yield f
elif field is False:
raise
except StopIteration:
field = self._stopFeeding()
if isinstance(field, Field):
yield field
elif hasattr(field, '__iter__'):
for f in field:
yield f
def _isDone(self):
return (self._field_generator is None)
done = property(_isDone, doc="Boolean to know if parsing is done or not")
#
# FieldSet_SeekUtility
#
def seekBit(self, address, name="padding[]",
description=None, relative=True, null=False):
"""
Create a field to seek to specified address,
or None if it's not needed.
May raise an (ParserError) exception if address is invalid.
"""
if relative:
nbits = address - self._current_size
else:
nbits = address - (self.absolute_address + self._current_size)
if nbits < 0:
raise ParserError("Seek error, unable to go back!")
if 0 < nbits:
if null:
return createNullField(self, nbits, name, description)
else:
return createPaddingField(self, nbits, name, description)
else:
return None
def seekByte(self, address, name="padding[]", description=None, relative=True, null=False):
"""
Same as seekBit(), but with address in byte.
"""
return self.seekBit(address * 8, name, description, relative, null=null)
#
# RandomAccessFieldSet
#
def replaceField(self, name, new_fields):
# TODO: Check in self and not self.field
# Problem is that "generator is already executing"
if name not in self._fields:
raise ParserError("Unable to replace %s: field doesn't exist!" % name)
assert 1 <= len(new_fields)
old_field = self[name]
total_size = sum( (field.size for field in new_fields) )
if old_field.size != total_size:
raise ParserError("Unable to replace %s: "
"new field(s) hasn't same size (%u bits instead of %u bits)!"
% (name, total_size, old_field.size))
field = new_fields[0]
if field._name.endswith("[]"):
self.setUniqueFieldName(field)
field._address = old_field.address
if field.name != name and field.name in self._fields:
raise ParserError(
"Unable to replace %s: name \"%s\" is already used!"
% (name, field.name))
self._fields.replace(name, field.name, field)
self.raiseEvent("field-replaced", old_field, field)
if 1 < len(new_fields):
index = self._fields.index(new_fields[0].name)+1
address = field.address + field.size
for field in new_fields[1:]:
if field._name.endswith("[]"):
self.setUniqueFieldName(field)
field._address = address
if field.name in self._fields:
raise ParserError(
"Unable to replace %s: name \"%s\" is already used!"
% (name, field.name))
self._fields.insert(index, field.name, field)
self.raiseEvent("field-inserted", index, field)
index += 1
address += field.size
def getFieldByAddress(self, address, feed=True):
"""
Only search in existing fields
"""
if feed and self._field_generator is not None:
self._feedAll()
if address < self._current_size:
i = lowerBound(self._fields.values, lambda x: x.address + x.size <= address)
if i is not None:
return self._fields.values[i]
return None
def writeFieldsIn(self, old_field, address, new_fields):
"""
Can only write in existing fields (address < self._current_size)
"""
# Check size
total_size = sum( field.size for field in new_fields )
if old_field.size < total_size:
raise ParserError( \
"Unable to write fields at address %s " \
"(too big)!" % (address))
# Need padding before?
replace = []
size = address - old_field.address
assert 0 <= size
if 0 < size:
padding = createPaddingField(self, size)
padding._address = old_field.address
replace.append(padding)
# Set fields address
for field in new_fields:
field._address = address
address += field.size
replace.append(field)
# Need padding after?
size = (old_field.address + old_field.size) - address
assert 0 <= size
if 0 < size:
padding = createPaddingField(self, size)
padding._address = address
replace.append(padding)
self.replaceField(old_field.name, replace)
def nextFieldAddress(self):
return self._current_size
def getFieldIndex(self, field):
return self._fields.index(field._name)
-57
View File
@@ -1,57 +0,0 @@
from hachoir_core.field import (FieldError,
RawBits, RawBytes,
PaddingBits, PaddingBytes,
NullBits, NullBytes,
GenericString, GenericInteger)
from hachoir_core.stream import FileOutputStream
def createRawField(parent, size, name="raw[]", description=None):
if size <= 0:
raise FieldError("Unable to create raw field of %s bits" % size)
if (size % 8) == 0:
return RawBytes(parent, name, size/8, description)
else:
return RawBits(parent, name, size, description)
def createPaddingField(parent, nbits, name="padding[]", description=None):
if nbits <= 0:
raise FieldError("Unable to create padding of %s bits" % nbits)
if (nbits % 8) == 0:
return PaddingBytes(parent, name, nbits/8, description)
else:
return PaddingBits(parent, name, nbits, description)
def createNullField(parent, nbits, name="padding[]", description=None):
if nbits <= 0:
raise FieldError("Unable to create null padding of %s bits" % nbits)
if (nbits % 8) == 0:
return NullBytes(parent, name, nbits/8, description)
else:
return NullBits(parent, name, nbits, description)
def isString(field):
return issubclass(field.__class__, GenericString)
def isInteger(field):
return issubclass(field.__class__, GenericInteger)
def writeIntoFile(fieldset, filename):
output = FileOutputStream(filename)
fieldset.writeInto(output)
def createOrphanField(fieldset, address, field_cls, *args, **kw):
"""
Create an orphan field at specified address:
field_cls(fieldset, *args, **kw)
The field uses the fieldset properties but it isn't added to the
field set.
"""
save_size = fieldset._current_size
try:
fieldset._current_size = address
field = field_cls(fieldset, *args, **kw)
finally:
fieldset._current_size = save_size
return field
-44
View File
@@ -1,44 +0,0 @@
"""
Integer field classes:
- UInt8, UInt16, UInt24, UInt32, UInt64: unsigned integer of 8, 16, 32, 64 bits ;
- Int8, Int16, Int24, Int32, Int64: signed integer of 8, 16, 32, 64 bits.
"""
from hachoir_core.field import Bits, FieldError
class GenericInteger(Bits):
"""
Generic integer class used to generate other classes.
"""
def __init__(self, parent, name, signed, size, description=None):
if not (8 <= size <= 256):
raise FieldError("Invalid integer size (%s): have to be in 8..256" % size)
Bits.__init__(self, parent, name, size, description)
self.signed = signed
def createValue(self):
return self._parent.stream.readInteger(
self.absolute_address, self.signed, self._size, self._parent.endian)
def integerFactory(name, is_signed, size, doc):
class Integer(GenericInteger):
__doc__ = doc
static_size = size
def __init__(self, parent, name, description=None):
GenericInteger.__init__(self, parent, name, is_signed, size, description)
cls = Integer
cls.__name__ = name
return cls
UInt8 = integerFactory("UInt8", False, 8, "Unsigned integer of 8 bits")
UInt16 = integerFactory("UInt16", False, 16, "Unsigned integer of 16 bits")
UInt24 = integerFactory("UInt24", False, 24, "Unsigned integer of 24 bits")
UInt32 = integerFactory("UInt32", False, 32, "Unsigned integer of 32 bits")
UInt64 = integerFactory("UInt64", False, 64, "Unsigned integer of 64 bits")
Int8 = integerFactory("Int8", True, 8, "Signed integer of 8 bits")
Int16 = integerFactory("Int16", True, 16, "Signed integer of 16 bits")
Int24 = integerFactory("Int24", True, 24, "Signed integer of 24 bits")
Int32 = integerFactory("Int32", True, 32, "Signed integer of 32 bits")
Int64 = integerFactory("Int64", True, 64, "Signed integer of 64 bits")
-109
View File
@@ -1,109 +0,0 @@
from hachoir_core.field import Field, FieldSet, ParserError, Bytes, MissingField
from hachoir_core.stream import FragmentedStream
class Link(Field):
def __init__(self, parent, name, *args, **kw):
Field.__init__(self, parent, name, 0, *args, **kw)
def hasValue(self):
return True
def createValue(self):
return self._parent[self.display]
def createDisplay(self):
value = self.value
if value is None:
return "<%s>" % MissingField.__name__
return value.path
def _getField(self, name, const):
target = self.value
assert self != target
return target._getField(name, const)
class Fragments:
def __init__(self, first):
self.first = first
def __iter__(self):
fragment = self.first
while fragment is not None:
data = fragment.getData()
yield data and data.size
fragment = fragment.next
class Fragment(FieldSet):
_first = None
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._field_generator = self._createFields(self._field_generator)
if self.__class__.createFields == Fragment.createFields:
self._getData = lambda: self
def getData(self):
try:
return self._getData()
except MissingField, e:
self.error(str(e))
return None
def setLinks(self, first, next=None):
self._first = first or self
self._next = next
self._feedLinks = lambda: self
return self
def _feedLinks(self):
while self._first is None and self.readMoreFields(1):
pass
if self._first is None:
raise ParserError("first is None")
return self
first = property(lambda self: self._feedLinks()._first)
def _getNext(self):
next = self._feedLinks()._next
if callable(next):
self._next = next = next()
return next
next = property(_getNext)
def _createInputStream(self, **args):
first = self.first
if first is self and hasattr(first, "_getData"):
return FragmentedStream(first, packets=Fragments(first), **args)
return FieldSet._createInputStream(self, **args)
def _createFields(self, field_generator):
if self._first is None:
for field in field_generator:
if self._first is not None:
break
yield field
else:
raise ParserError("Fragment.setLinks not called")
else:
field = None
if self._first is not self:
link = Link(self, "first", None)
link._getValue = lambda: self._first
yield link
if self._next:
link = Link(self, "next", None)
link.createValue = self._getNext
yield link
if field:
yield field
for field in field_generator:
yield field
def createFields(self):
if self._size is None:
self._size = self._getSize()
yield Bytes(self, "data", self._size/8)
@@ -1,82 +0,0 @@
from hachoir_core.field import BasicFieldSet, GenericFieldSet, ParserError, createRawField
from hachoir_core.error import HACHOIR_ERRORS
# getgaps(int, int, [listof (int, int)]) -> generator of (int, int)
# Gets all the gaps not covered by a block in `blocks` from `start` for `length` units.
def getgaps(start, length, blocks):
'''
Example:
>>> list(getgaps(0, 20, [(15,3), (6,2), (6,2), (1,2), (2,3), (11,2), (9,5)]))
[(0, 1), (5, 1), (8, 1), (14, 1), (18, 2)]
'''
# done this way to avoid mutating the original
blocks = sorted(blocks, key=lambda b: b[0])
end = start+length
for s, l in blocks:
if s > start:
yield (start, s-start)
start = s
if s+l > start:
start = s+l
if start < end:
yield (start, end-start)
class NewRootSeekableFieldSet(GenericFieldSet):
def seekBit(self, address, relative=True):
if not relative:
address -= self.absolute_address
if address < 0:
raise ParserError("Seek below field set start (%s.%s)" % divmod(address, 8))
self._current_size = address
return None
def seekByte(self, address, relative=True):
return self.seekBit(address*8, relative)
def _fixLastField(self):
"""
Try to fix last field when we know current field set size.
Returns new added field if any, or None.
"""
assert self._size is not None
# Stop parser
message = ["stop parser"]
self._field_generator = None
# If last field is too big, delete it
while self._size < self._current_size:
field = self._deleteField(len(self._fields)-1)
message.append("delete field %s" % field.path)
assert self._current_size <= self._size
blocks = [(x.absolute_address, x.size) for x in self._fields]
fields = []
for start, length in getgaps(self.absolute_address, self._size, blocks):
self.seekBit(start, relative=False)
field = createRawField(self, length, "unparsed[]")
self.setUniqueFieldName(field)
self._fields.append(field.name, field)
fields.append(field)
message.append("found unparsed segment: start %s, length %s" % (start, length))
self.seekBit(self._size, relative=False)
message = ", ".join(message)
if fields:
self.warning("[Autofix] Fix parser error: " + message)
return fields
def _stopFeeding(self):
new_field = None
if self._size is None:
if self._parent:
self._size = self._current_size
new_field = self._fixLastField()
self._field_generator = None
return new_field
class NewSeekableFieldSet(NewRootSeekableFieldSet):
def __init__(self, parent, name, description=None, size=None):
assert issubclass(parent.__class__, BasicFieldSet)
NewRootSeekableFieldSet.__init__(self, parent, name, parent.stream, description, size)
-138
View File
@@ -1,138 +0,0 @@
from hachoir_core.field import Bits, Bytes
from hachoir_core.tools import makePrintable, humanFilesize
from hachoir_core import config
class PaddingBits(Bits):
"""
Padding bits used, for example, to align address (of next field).
See also NullBits and PaddingBytes types.
Arguments:
* nbits: Size of the field in bits
Optional arguments:
* pattern (int): Content pattern, eg. 0 if all bits are set to 0
"""
static_size = staticmethod(lambda *args, **kw: args[1])
MAX_SIZE = 128
def __init__(self, parent, name, nbits, description="Padding", pattern=None):
Bits.__init__(self, parent, name, nbits, description)
self.pattern = pattern
self._display_pattern = self.checkPattern()
def checkPattern(self):
if not(config.check_padding_pattern):
return False
if self.pattern != 0:
return False
if self.MAX_SIZE < self._size:
value = self._parent.stream.readBits(
self.absolute_address, self.MAX_SIZE, self._parent.endian)
else:
value = self.value
if value != 0:
self.warning("padding contents doesn't look normal (invalid pattern)")
return False
if self.MAX_SIZE < self._size:
self.info("only check first %u bits" % self.MAX_SIZE)
return True
def createDisplay(self):
if self._display_pattern:
return u"<padding pattern=%s>" % self.pattern
else:
return Bits.createDisplay(self)
class PaddingBytes(Bytes):
"""
Padding bytes used, for example, to align address (of next field).
See also NullBytes and PaddingBits types.
Arguments:
* nbytes: Size of the field in bytes
Optional arguments:
* pattern (str): Content pattern, eg. "\0" for nul bytes
"""
static_size = staticmethod(lambda *args, **kw: args[1]*8)
MAX_SIZE = 4096
def __init__(self, parent, name, nbytes,
description="Padding", pattern=None):
""" pattern is None or repeated string """
assert (pattern is None) or (isinstance(pattern, str))
Bytes.__init__(self, parent, name, nbytes, description)
self.pattern = pattern
self._display_pattern = self.checkPattern()
def checkPattern(self):
if not(config.check_padding_pattern):
return False
if self.pattern is None:
return False
if self.MAX_SIZE < self._size/8:
self.info("only check first %s of padding" % humanFilesize(self.MAX_SIZE))
content = self._parent.stream.readBytes(
self.absolute_address, self.MAX_SIZE)
else:
content = self.value
index = 0
pattern_len = len(self.pattern)
while index < len(content):
if content[index:index+pattern_len] != self.pattern:
self.warning(
"padding contents doesn't look normal"
" (invalid pattern at byte %u)!"
% index)
return False
index += pattern_len
return True
def createDisplay(self):
if self._display_pattern:
return u"<padding pattern=%s>" % makePrintable(self.pattern, "ASCII", quote="'")
else:
return Bytes.createDisplay(self)
def createRawDisplay(self):
return Bytes.createDisplay(self)
class NullBits(PaddingBits):
"""
Null padding bits used, for example, to align address (of next field).
See also PaddingBits and NullBytes types.
Arguments:
* nbits: Size of the field in bits
"""
def __init__(self, parent, name, nbits, description=None):
PaddingBits.__init__(self, parent, name, nbits, description, pattern=0)
def createDisplay(self):
if self._display_pattern:
return "<null>"
else:
return Bits.createDisplay(self)
class NullBytes(PaddingBytes):
"""
Null padding bytes used, for example, to align address (of next field).
See also PaddingBytes and NullBits types.
Arguments:
* nbytes: Size of the field in bytes
"""
def __init__(self, parent, name, nbytes, description=None):
PaddingBytes.__init__(self, parent, name, nbytes, description, pattern="\0")
def createDisplay(self):
if self._display_pattern:
return "<null>"
else:
return Bytes.createDisplay(self)
-40
View File
@@ -1,40 +0,0 @@
from hachoir_core.endian import BIG_ENDIAN, LITTLE_ENDIAN
from hachoir_core.field import GenericFieldSet
from hachoir_core.log import Logger
import hachoir_core.config as config
class Parser(GenericFieldSet):
"""
A parser is the root of all other fields. It create first level of fields
and have special attributes and methods:
- endian: Byte order (L{BIG_ENDIAN} or L{LITTLE_ENDIAN}) of input data ;
- stream: Data input stream (set in L{__init__()}) ;
- size: Field set size will be size of input stream.
"""
def __init__(self, stream, description=None):
"""
Parser constructor
@param stream: Data input stream (see L{InputStream})
@param description: (optional) String description
"""
# Check arguments
assert hasattr(self, "endian") \
and self.endian in (BIG_ENDIAN, LITTLE_ENDIAN)
# Call parent constructor
GenericFieldSet.__init__(self, None, "root", stream, description, stream.askSize(self))
def _logger(self):
return Logger._logger(self)
def _setSize(self, size):
self._truncate(size)
self.raiseEvent("field-resized", self)
size = property(lambda self: self._size, doc="Size in bits")
path = property(lambda self: "/")
# dummy definition to prevent hachoir-core from depending on hachoir-parser
autofix = property(lambda self: config.autofix)
@@ -1,182 +0,0 @@
from hachoir_core.field import Field, BasicFieldSet, FakeArray, MissingField, ParserError
from hachoir_core.tools import makeUnicode
from hachoir_core.error import HACHOIR_ERRORS
from itertools import repeat
import hachoir_core.config as config
class RootSeekableFieldSet(BasicFieldSet):
def __init__(self, parent, name, stream, description, size):
BasicFieldSet.__init__(self, parent, name, stream, description, size)
self._generator = self.createFields()
self._offset = 0
self._current_size = 0
if size:
self._current_max_size = size
else:
self._current_max_size = 0
self._field_dict = {}
self._field_array = []
def _feedOne(self):
assert self._generator
field = self._generator.next()
self._addField(field)
return field
def array(self, key):
return FakeArray(self, key)
def getFieldByAddress(self, address, feed=True):
for field in self._field_array:
if field.address <= address < field.address + field.size:
return field
for field in self._readFields():
if field.address <= address < field.address + field.size:
return field
return None
def _stopFeed(self):
self._size = self._current_max_size
self._generator = None
done = property(lambda self: not bool(self._generator))
def _getSize(self):
if self._size is None:
self._feedAll()
return self._size
size = property(_getSize)
def _getField(self, key, const):
field = Field._getField(self, key, const)
if field is not None:
return field
if key in self._field_dict:
return self._field_dict[key]
if self._generator and not const:
try:
while True:
field = self._feedOne()
if field.name == key:
return field
except StopIteration:
self._stopFeed()
except HACHOIR_ERRORS, err:
self.error("Error: %s" % makeUnicode(err))
self._stopFeed()
return None
def getField(self, key, const=True):
if isinstance(key, (int, long)):
if key < 0:
raise KeyError("Key must be positive!")
if not const:
self.readFirstFields(key+1)
if len(self._field_array) <= key:
raise MissingField(self, key)
return self._field_array[key]
return Field.getField(self, key, const)
def _addField(self, field):
if field._name.endswith("[]"):
self.setUniqueFieldName(field)
if config.debug:
self.info("[+] DBG: _addField(%s)" % field.name)
if field._address != self._offset:
self.warning("Set field %s address to %s (was %s)" % (
field.path, self._offset//8, field._address//8))
field._address = self._offset
assert field.name not in self._field_dict
self._checkFieldSize(field)
self._field_dict[field.name] = field
self._field_array.append(field)
self._current_size += field.size
self._offset += field.size
self._current_max_size = max(self._current_max_size, field.address + field.size)
def _checkAddress(self, address):
if self._size is not None:
max_addr = self._size
else:
# FIXME: Use parent size
max_addr = self.stream.size
return address < max_addr
def _checkFieldSize(self, field):
size = field.size
addr = field.address
if not self._checkAddress(addr+size-1):
raise ParserError("Unable to add %s: field is too large" % field.name)
def seekBit(self, address, relative=True):
if not relative:
address -= self.absolute_address
if address < 0:
raise ParserError("Seek below field set start (%s.%s)" % divmod(address, 8))
if not self._checkAddress(address):
raise ParserError("Seek above field set end (%s.%s)" % divmod(address, 8))
self._offset = address
return None
def seekByte(self, address, relative=True):
return self.seekBit(address*8, relative)
def readMoreFields(self, number):
return self._readMoreFields(xrange(number))
def _feedAll(self):
return self._readMoreFields(repeat(1))
def _readFields(self):
while True:
added = self._readMoreFields(xrange(1))
if not added:
break
yield self._field_array[-1]
def _readMoreFields(self, index_generator):
added = 0
if self._generator:
try:
for index in index_generator:
self._feedOne()
added += 1
except StopIteration:
self._stopFeed()
except HACHOIR_ERRORS, err:
self.error("Error: %s" % makeUnicode(err))
self._stopFeed()
return added
current_length = property(lambda self: len(self._field_array))
current_size = property(lambda self: self._offset)
def __iter__(self):
for field in self._field_array:
yield field
if self._generator:
try:
while True:
yield self._feedOne()
except StopIteration:
self._stopFeed()
raise StopIteration
def __len__(self):
if self._generator:
self._feedAll()
return len(self._field_array)
def nextFieldAddress(self):
return self._offset
def getFieldIndex(self, field):
return self._field_array.index(field)
class SeekableFieldSet(RootSeekableFieldSet):
def __init__(self, parent, name, description=None, size=None):
assert issubclass(parent.__class__, BasicFieldSet)
RootSeekableFieldSet.__init__(self, parent, name, parent.stream, description, size)
@@ -1,54 +0,0 @@
from hachoir_core.field import FieldSet, ParserError
class StaticFieldSet(FieldSet):
"""
Static field set: format class attribute is a tuple of all fields
in syntax like:
format = (
(TYPE1, ARG1, ARG2, ...),
(TYPE2, ARG1, ARG2, ..., {KEY1=VALUE1, ...}),
...
)
Types with dynamic size are forbidden, eg. CString, PascalString8, etc.
"""
format = None # You have to redefine this class variable
_class = None
def __new__(cls, *args, **kw):
assert cls.format is not None, "Class attribute 'format' is not set"
if cls._class is not cls.__name__:
cls._class = cls.__name__
cls.static_size = cls._computeStaticSize()
return object.__new__(cls, *args, **kw)
@staticmethod
def _computeItemSize(item):
item_class = item[0]
if item_class.static_size is None:
raise ParserError("Unable to get static size of field type: %s"
% item_class.__name__)
if callable(item_class.static_size):
if isinstance(item[-1], dict):
return item_class.static_size(*item[1:-1], **item[-1])
else:
return item_class.static_size(*item[1:])
else:
assert isinstance(item_class.static_size, (int, long))
return item_class.static_size
def createFields(self):
for item in self.format:
if isinstance(item[-1], dict):
yield item[0](self, *item[1:-1], **item[-1])
else:
yield item[0](self, *item[1:])
@classmethod
def _computeStaticSize(cls, *args):
return sum(cls._computeItemSize(item) for item in cls.format)
# Initial value of static_size, it changes when first instance
# is created (see __new__)
static_size = _computeStaticSize
-402
View File
@@ -1,402 +0,0 @@
"""
String field classes:
- String: Fixed length string (no prefix/no suffix) ;
- CString: String which ends with nul byte ("\0") ;
- UnixLine: Unix line of text, string which ends with "\n" ;
- PascalString8, PascalString16, PascalString32: String prefixed with
length written in a 8, 16, 32-bit integer (use parent endian).
Constructor has optional arguments:
- strip: value can be a string or True ;
- charset: if set, convert string to unicode using this charset (in "replace"
mode which replace all buggy characters with ".").
Note: For PascalStringXX, prefixed value is the number of bytes and not
of characters!
"""
from hachoir_core.field import FieldError, Bytes
from hachoir_core.endian import LITTLE_ENDIAN, BIG_ENDIAN
from hachoir_core.tools import alignValue, makePrintable
from hachoir_core.i18n import guessBytesCharset, _
from hachoir_core import config
from codecs import BOM_UTF16_LE, BOM_UTF16_BE, BOM_UTF32_LE, BOM_UTF32_BE
# Default charset used to convert byte string to Unicode
# This charset is used if no charset is specified or on conversion error
FALLBACK_CHARSET = "ISO-8859-1"
class GenericString(Bytes):
"""
Generic string class.
charset have to be in CHARSET_8BIT or in UTF_CHARSET.
"""
VALID_FORMATS = ("C", "UnixLine",
"fixed", "Pascal8", "Pascal16", "Pascal32")
# 8-bit charsets
CHARSET_8BIT = set((
"ASCII", # ANSI X3.4-1968
"MacRoman",
"CP037", # EBCDIC 037
"CP874", # Thai
"WINDOWS-1250", # Central Europe
"WINDOWS-1251", # Cyrillic
"WINDOWS-1252", # Latin I
"WINDOWS-1253", # Greek
"WINDOWS-1254", # Turkish
"WINDOWS-1255", # Hebrew
"WINDOWS-1256", # Arabic
"WINDOWS-1257", # Baltic
"WINDOWS-1258", # Vietnam
"ISO-8859-1", # Latin-1
"ISO-8859-2", # Latin-2
"ISO-8859-3", # Latin-3
"ISO-8859-4", # Latin-4
"ISO-8859-5",
"ISO-8859-6",
"ISO-8859-7",
"ISO-8859-8",
"ISO-8859-9", # Latin-5
"ISO-8859-10", # Latin-6
"ISO-8859-11", # Thai
"ISO-8859-13", # Latin-7
"ISO-8859-14", # Latin-8
"ISO-8859-15", # Latin-9 or ("Latin-0")
"ISO-8859-16", # Latin-10
))
# UTF-xx charset familly
UTF_CHARSET = {
"UTF-8": (8, None),
"UTF-16-LE": (16, LITTLE_ENDIAN),
"UTF-32LE": (32, LITTLE_ENDIAN),
"UTF-16-BE": (16, BIG_ENDIAN),
"UTF-32BE": (32, BIG_ENDIAN),
"UTF-16": (16, "BOM"),
"UTF-32": (32, "BOM"),
}
# UTF-xx BOM => charset with endian
UTF_BOM = {
16: {BOM_UTF16_LE: "UTF-16-LE", BOM_UTF16_BE: "UTF-16-BE"},
32: {BOM_UTF32_LE: "UTF-32LE", BOM_UTF32_BE: "UTF-32BE"},
}
# Suffix format: value is suffix (string)
SUFFIX_FORMAT = {
"C": {
8: {LITTLE_ENDIAN: "\0", BIG_ENDIAN: "\0"},
16: {LITTLE_ENDIAN: "\0\0", BIG_ENDIAN: "\0\0"},
32: {LITTLE_ENDIAN: "\0\0\0\0", BIG_ENDIAN: "\0\0\0\0"},
},
"UnixLine": {
8: {LITTLE_ENDIAN: "\n", BIG_ENDIAN: "\n"},
16: {LITTLE_ENDIAN: "\n\0", BIG_ENDIAN: "\0\n"},
32: {LITTLE_ENDIAN: "\n\0\0\0", BIG_ENDIAN: "\0\0\0\n"},
},
}
# Pascal format: value is the size of the prefix in bits
PASCAL_FORMATS = {
"Pascal8": 1,
"Pascal16": 2,
"Pascal32": 4
}
# Raw value: with prefix and suffix, not stripped,
# and not converted to Unicode
_raw_value = None
def __init__(self, parent, name, format, description=None,
strip=None, charset=None, nbytes=None, truncate=None):
Bytes.__init__(self, parent, name, 1, description)
# Is format valid?
assert format in self.VALID_FORMATS
# Store options
self._format = format
self._strip = strip
self._truncate = truncate
# Check charset and compute character size in bytes
# (or None when it's not possible to guess character size)
if not charset or charset in self.CHARSET_8BIT:
self._character_size = 1 # one byte per character
elif charset in self.UTF_CHARSET:
self._character_size = None
else:
raise FieldError("Invalid charset for %s: \"%s\"" %
(self.path, charset))
self._charset = charset
# It is a fixed string?
if nbytes is not None:
assert self._format == "fixed"
# Arbitrary limits, just to catch some bugs...
if not (1 <= nbytes <= 0xffff):
raise FieldError("Invalid string size for %s: %s" %
(self.path, nbytes))
self._content_size = nbytes # content length in bytes
self._size = nbytes * 8
self._content_offset = 0
else:
# Format with a suffix: Find the end of the string
if self._format in self.SUFFIX_FORMAT:
self._content_offset = 0
# Choose the suffix
suffix = self.suffix_str
# Find the suffix
length = self._parent.stream.searchBytesLength(
suffix, False, self.absolute_address)
if length is None:
raise FieldError("Unable to find end of string %s (format %s)!"
% (self.path, self._format))
if 1 < len(suffix):
# Fix length for little endian bug with UTF-xx charset:
# u"abc" -> "a\0b\0c\0\0\0" (UTF-16-LE)
# search returns length=5, whereas real lenght is 6
length = alignValue(length, len(suffix))
# Compute sizes
self._content_size = length # in bytes
self._size = (length + len(suffix)) * 8
# Format with a prefix: Read prefixed length in bytes
else:
assert self._format in self.PASCAL_FORMATS
# Get the prefix size
prefix_size = self.PASCAL_FORMATS[self._format]
self._content_offset = prefix_size
# Read the prefix and compute sizes
value = self._parent.stream.readBits(
self.absolute_address, prefix_size*8, self._parent.endian)
self._content_size = value # in bytes
self._size = (prefix_size + value) * 8
# For UTF-16 and UTF-32, choose the right charset using BOM
if self._charset in self.UTF_CHARSET:
# Charset requires a BOM?
bomsize, endian = self.UTF_CHARSET[self._charset]
if endian == "BOM":
# Read the BOM value
nbytes = bomsize // 8
bom = self._parent.stream.readBytes(self.absolute_address, nbytes)
# Choose right charset using the BOM
bom_endian = self.UTF_BOM[bomsize]
if bom not in bom_endian:
raise FieldError("String %s has invalid BOM (%s)!"
% (self.path, repr(bom)))
self._charset = bom_endian[bom]
self._content_size -= nbytes
self._content_offset += nbytes
# Compute length in character if possible
if self._character_size:
self._length = self._content_size // self._character_size
else:
self._length = None
@staticmethod
def staticSuffixStr(format, charset, endian):
if format not in GenericString.SUFFIX_FORMAT:
return ''
suffix = GenericString.SUFFIX_FORMAT[format]
if charset in GenericString.UTF_CHARSET:
suffix_size = GenericString.UTF_CHARSET[charset][0]
suffix = suffix[suffix_size]
else:
suffix = suffix[8]
return suffix[endian]
def _getSuffixStr(self):
return self.staticSuffixStr(
self._format, self._charset, self._parent.endian)
suffix_str = property(_getSuffixStr)
def _convertText(self, text):
if not self._charset:
# charset is still unknown: guess the charset
self._charset = guessBytesCharset(text, default=FALLBACK_CHARSET)
# Try to convert to Unicode
try:
return unicode(text, self._charset, "strict")
except UnicodeDecodeError, err:
pass
#--- Conversion error ---
# Fix truncated UTF-16 string like 'B\0e' (3 bytes)
# => Add missing nul byte: 'B\0e\0' (4 bytes)
if err.reason == "truncated data" \
and err.end == len(text) \
and self._charset == "UTF-16-LE":
try:
text = unicode(text+"\0", self._charset, "strict")
self.warning("Fix truncated %s string: add missing nul byte" % self._charset)
return text
except UnicodeDecodeError, err:
pass
# On error, use FALLBACK_CHARSET
self.warning(u"Unable to convert string to Unicode: %s" % err)
return unicode(text, FALLBACK_CHARSET, "strict")
def _guessCharset(self):
addr = self.absolute_address + self._content_offset * 8
bytes = self._parent.stream.readBytes(addr, self._content_size)
return guessBytesCharset(bytes, default=FALLBACK_CHARSET)
def createValue(self, human=True):
# Compress data address (in bits) and size (in bytes)
if human:
addr = self.absolute_address + self._content_offset * 8
size = self._content_size
else:
addr = self.absolute_address
size = self._size // 8
if size == 0:
# Empty string
return u""
# Read bytes in data stream
text = self._parent.stream.readBytes(addr, size)
# Don't transform data?
if not human:
return text
# Convert text to Unicode
text = self._convertText(text)
# Truncate
if self._truncate:
pos = text.find(self._truncate)
if 0 <= pos:
text = text[:pos]
# Strip string if needed
if self._strip:
if isinstance(self._strip, (str, unicode)):
text = text.strip(self._strip)
else:
text = text.strip()
assert isinstance(text, unicode)
return text
def createDisplay(self, human=True):
if not human:
if self._raw_value is None:
self._raw_value = GenericString.createValue(self, False)
value = makePrintable(self._raw_value, "ASCII", to_unicode=True)
elif self._charset:
value = makePrintable(self.value, "ISO-8859-1", to_unicode=True)
else:
value = self.value
if config.max_string_length < len(value):
# Truncate string if needed
value = "%s(...)" % value[:config.max_string_length]
if not self._charset or not human:
return makePrintable(value, "ASCII", quote='"', to_unicode=True)
else:
if value:
return '"%s"' % value.replace('"', '\\"')
else:
return _("(empty)")
def createRawDisplay(self):
return GenericString.createDisplay(self, human=False)
def _getLength(self):
if self._length is None:
self._length = len(self.value)
return self._length
length = property(_getLength, doc="String length in characters")
def _getFormat(self):
return self._format
format = property(_getFormat, doc="String format (eg. 'C')")
def _getCharset(self):
if not self._charset:
self._charset = self._guessCharset()
return self._charset
charset = property(_getCharset, doc="String charset (eg. 'ISO-8859-1')")
def _getContentSize(self):
return self._content_size
content_size = property(_getContentSize, doc="Content size in bytes")
def _getContentOffset(self):
return self._content_offset
content_offset = property(_getContentOffset, doc="Content offset in bytes")
def getFieldType(self):
info = self.charset
if self._strip:
if isinstance(self._strip, (str, unicode)):
info += ",strip=%s" % makePrintable(self._strip, "ASCII", quote="'")
else:
info += ",strip=True"
return "%s<%s>" % (Bytes.getFieldType(self), info)
def stringFactory(name, format, doc):
class NewString(GenericString):
__doc__ = doc
def __init__(self, parent, name, description=None,
strip=None, charset=None, truncate=None):
GenericString.__init__(self, parent, name, format, description,
strip=strip, charset=charset, truncate=truncate)
cls = NewString
cls.__name__ = name
return cls
# String which ends with nul byte ("\0")
CString = stringFactory("CString", "C",
r"""C string: string ending with nul byte.
See GenericString to get more information.""")
# Unix line of text: string which ends with "\n" (ASCII 0x0A)
UnixLine = stringFactory("UnixLine", "UnixLine",
r"""Unix line: string ending with "\n" (ASCII code 10).
See GenericString to get more information.""")
# String prefixed with length written in a 8-bit integer
PascalString8 = stringFactory("PascalString8", "Pascal8",
r"""Pascal string: string prefixed with 8-bit integer containing its length (endian depends on parent endian).
See GenericString to get more information.""")
# String prefixed with length written in a 16-bit integer (use parent endian)
PascalString16 = stringFactory("PascalString16", "Pascal16",
r"""Pascal string: string prefixed with 16-bit integer containing its length (endian depends on parent endian).
See GenericString to get more information.""")
# String prefixed with length written in a 32-bit integer (use parent endian)
PascalString32 = stringFactory("PascalString32", "Pascal32",
r"""Pascal string: string prefixed with 32-bit integer containing its length (endian depends on parent endian).
See GenericString to get more information.""")
class String(GenericString):
"""
String with fixed size (size in bytes).
See GenericString to get more information.
"""
static_size = staticmethod(lambda *args, **kw: args[1]*8)
def __init__(self, parent, name, nbytes, description=None,
strip=None, charset=None, truncate=None):
GenericString.__init__(self, parent, name, "fixed", description,
strip=strip, charset=charset, nbytes=nbytes, truncate=truncate)
String.__name__ = "FixedString"
-72
View File
@@ -1,72 +0,0 @@
from hachoir_core.field import Bytes
from hachoir_core.tools import makePrintable, humanFilesize
from hachoir_core.stream import InputIOStream
class SubFile(Bytes):
"""
File stored in another file
"""
def __init__(self, parent, name, length, description=None,
parser=None, filename=None, mime_type=None, parser_class=None):
if filename:
if not isinstance(filename, unicode):
filename = makePrintable(filename, "ISO-8859-1")
if not description:
description = 'File "%s" (%s)' % (filename, humanFilesize(length))
Bytes.__init__(self, parent, name, length, description)
def createInputStream(cis, **args):
tags = args.setdefault("tags",[])
if parser_class:
tags.append(( "class", parser_class ))
if parser is not None:
tags.append(( "id", parser.PARSER_TAGS["id"] ))
if mime_type:
tags.append(( "mime", mime_type ))
if filename:
tags.append(( "filename", filename ))
return cis(**args)
self.setSubIStream(createInputStream)
class CompressedStream:
offset = 0
def __init__(self, stream, decompressor):
self.stream = stream
self.decompressor = decompressor(stream)
self._buffer = ''
def read(self, size):
d = self._buffer
data = [ d[:size] ]
size -= len(d)
if size > 0:
d = self.decompressor(size)
data.append(d[:size])
size -= len(d)
while size > 0:
n = 4096
if self.stream.size:
n = min(self.stream.size - self.offset, n)
if not n:
break
d = self.stream.read(self.offset, n)[1]
self.offset += 8 * len(d)
d = self.decompressor(size, d)
data.append(d[:size])
size -= len(d)
self._buffer = d[size+len(d):]
return ''.join(data)
def CompressedField(field, decompressor):
def createInputStream(cis, source=None, **args):
if field._parent:
stream = cis(source=source)
args.setdefault("tags", []).extend(stream.tags)
else:
stream = field.stream
input = CompressedStream(stream, decompressor)
if source is None:
source = "Compressed source: '%s' (offset=%s)" % (stream.source, field.absolute_address)
return InputIOStream(input, source=source, **args)
field.setSubIStream(createInputStream)
return field
-86
View File
@@ -1,86 +0,0 @@
from hachoir_core.tools import (humanDatetime, humanDuration,
timestampUNIX, timestampMac32, timestampUUID60,
timestampWin64, durationWin64)
from hachoir_core.field import Bits, FieldSet
from datetime import datetime
class GenericTimestamp(Bits):
def __init__(self, parent, name, size, description=None):
Bits.__init__(self, parent, name, size, description)
def createDisplay(self):
return humanDatetime(self.value)
def createRawDisplay(self):
value = Bits.createValue(self)
return unicode(value)
def __nonzero__(self):
return Bits.createValue(self) != 0
def timestampFactory(cls_name, handler, size):
class Timestamp(GenericTimestamp):
def __init__(self, parent, name, description=None):
GenericTimestamp.__init__(self, parent, name, size, description)
def createValue(self):
value = Bits.createValue(self)
return handler(value)
cls = Timestamp
cls.__name__ = cls_name
return cls
TimestampUnix32 = timestampFactory("TimestampUnix32", timestampUNIX, 32)
TimestampUnix64 = timestampFactory("TimestampUnix64", timestampUNIX, 64)
TimestampMac32 = timestampFactory("TimestampUnix32", timestampMac32, 32)
TimestampUUID60 = timestampFactory("TimestampUUID60", timestampUUID60, 60)
TimestampWin64 = timestampFactory("TimestampWin64", timestampWin64, 64)
class TimeDateMSDOS32(FieldSet):
"""
32-bit MS-DOS timestamp (16-bit time, 16-bit date)
"""
static_size = 32
def createFields(self):
# TODO: Create type "MSDOS_Second" : value*2
yield Bits(self, "second", 5, "Second/2")
yield Bits(self, "minute", 6)
yield Bits(self, "hour", 5)
yield Bits(self, "day", 5)
yield Bits(self, "month", 4)
# TODO: Create type "MSDOS_Year" : value+1980
yield Bits(self, "year", 7, "Number of year after 1980")
def createValue(self):
return datetime(
1980+self["year"].value, self["month"].value, self["day"].value,
self["hour"].value, self["minute"].value, 2*self["second"].value)
def createDisplay(self):
return humanDatetime(self.value)
class DateTimeMSDOS32(TimeDateMSDOS32):
"""
32-bit MS-DOS timestamp (16-bit date, 16-bit time)
"""
def createFields(self):
yield Bits(self, "day", 5)
yield Bits(self, "month", 4)
yield Bits(self, "year", 7, "Number of year after 1980")
yield Bits(self, "second", 5, "Second/2")
yield Bits(self, "minute", 6)
yield Bits(self, "hour", 5)
class TimedeltaWin64(GenericTimestamp):
def __init__(self, parent, name, description=None):
GenericTimestamp.__init__(self, parent, name, 64, description)
def createDisplay(self):
return humanDuration(self.value)
def createValue(self):
value = Bits.createValue(self)
return durationWin64(value)
-38
View File
@@ -1,38 +0,0 @@
from hachoir_core.field import Field, FieldSet, ParserError
class GenericVector(FieldSet):
def __init__(self, parent, name, nb_items, item_class, item_name="item", description=None):
# Sanity checks
assert issubclass(item_class, Field)
assert isinstance(item_class.static_size, (int, long))
if not(0 < nb_items):
raise ParserError('Unable to create empty vector "%s" in %s' \
% (name, parent.path))
size = nb_items * item_class.static_size
self.__nb_items = nb_items
self._item_class = item_class
self._item_name = item_name
FieldSet.__init__(self, parent, name, description, size=size)
def __len__(self):
return self.__nb_items
def createFields(self):
name = self._item_name + "[]"
parser = self._item_class
for index in xrange(len(self)):
yield parser(self, name)
class UserVector(GenericVector):
"""
To implement:
- item_name: name of a field without [] (eg. "color" becomes "color[0]"),
default value is "item"
- item_class: class of an item
"""
item_class = None
item_name = "item"
def __init__(self, parent, name, nb_items, description=None):
GenericVector.__init__(self, parent, name, nb_items, self.item_class, self.item_name, description)
-214
View File
@@ -1,214 +0,0 @@
# -*- coding: UTF-8 -*-
"""
Functions to manage internationalisation (i18n):
- initLocale(): setup locales and install Unicode compatible stdout and
stderr ;
- getTerminalCharset(): guess terminal charset ;
- gettext(text) translate a string to current language. The function always
returns Unicode string. You can also use the alias: _() ;
- ngettext(singular, plural, count): translate a sentence with singular and
plural form. The function always returns Unicode string.
WARNING: Loading this module indirectly calls initLocale() which sets
locale LC_ALL to ''. This is needed to get user preferred locale
settings.
"""
import hachoir_core.config as config
import hachoir_core
import locale
from os import path
import sys
from codecs import BOM_UTF8, BOM_UTF16_LE, BOM_UTF16_BE
def _getTerminalCharset():
"""
Function used by getTerminalCharset() to get terminal charset.
@see getTerminalCharset()
"""
# (1) Try locale.getpreferredencoding()
try:
charset = locale.getpreferredencoding()
if charset:
return charset
except (locale.Error, AttributeError):
pass
# (2) Try locale.nl_langinfo(CODESET)
try:
charset = locale.nl_langinfo(locale.CODESET)
if charset:
return charset
except (locale.Error, AttributeError):
pass
# (3) Try sys.stdout.encoding
if hasattr(sys.stdout, "encoding") and sys.stdout.encoding:
return sys.stdout.encoding
# (4) Otherwise, returns "ASCII"
return "ASCII"
def getTerminalCharset():
"""
Guess terminal charset using differents tests:
1. Try locale.getpreferredencoding()
2. Try locale.nl_langinfo(CODESET)
3. Try sys.stdout.encoding
4. Otherwise, returns "ASCII"
WARNING: Call initLocale() before calling this function.
"""
try:
return getTerminalCharset.value
except AttributeError:
getTerminalCharset.value = _getTerminalCharset()
return getTerminalCharset.value
class UnicodeStdout(object):
def __init__(self, old_device, charset):
self.device = old_device
self.charset = charset
def flush(self):
self.device.flush()
def write(self, text):
if isinstance(text, unicode):
text = text.encode(self.charset, 'replace')
self.device.write(text)
def writelines(self, lines):
for text in lines:
self.write(text)
def initLocale():
# Only initialize locale once
if initLocale.is_done:
return getTerminalCharset()
initLocale.is_done = True
# Setup locales
try:
locale.setlocale(locale.LC_ALL, "")
except (locale.Error, IOError):
pass
# Get the terminal charset
charset = getTerminalCharset()
# UnicodeStdout conflicts with the readline module
if config.unicode_stdout and ('readline' not in sys.modules):
# Replace stdout and stderr by unicode objet supporting unicode string
sys.stdout = UnicodeStdout(sys.stdout, charset)
sys.stderr = UnicodeStdout(sys.stderr, charset)
return charset
initLocale.is_done = False
def _dummy_gettext(text):
return unicode(text)
def _dummy_ngettext(singular, plural, count):
if 1 < abs(count) or not count:
return unicode(plural)
else:
return unicode(singular)
def _initGettext():
charset = initLocale()
# Try to load gettext module
if config.use_i18n:
try:
import gettext
ok = True
except ImportError:
ok = False
else:
ok = False
# gettext is not available or not needed: use dummy gettext functions
if not ok:
return (_dummy_gettext, _dummy_ngettext)
# Gettext variables
package = hachoir_core.PACKAGE
locale_dir = path.join(path.dirname(__file__), "..", "locale")
# Initialize gettext module
gettext.bindtextdomain(package, locale_dir)
gettext.textdomain(package)
translate = gettext.gettext
ngettext = gettext.ngettext
# TODO: translate_unicode lambda function really sucks!
# => find native function to do that
unicode_gettext = lambda text: \
unicode(translate(text), charset)
unicode_ngettext = lambda singular, plural, count: \
unicode(ngettext(singular, plural, count), charset)
return (unicode_gettext, unicode_ngettext)
UTF_BOMS = (
(BOM_UTF8, "UTF-8"),
(BOM_UTF16_LE, "UTF-16-LE"),
(BOM_UTF16_BE, "UTF-16-BE"),
)
# Set of valid characters for specific charset
CHARSET_CHARACTERS = (
# U+00E0: LATIN SMALL LETTER A WITH GRAVE
(set(u"©®éêè\xE0ç".encode("ISO-8859-1")), "ISO-8859-1"),
(set(u"©®éêè\xE0ç€".encode("ISO-8859-15")), "ISO-8859-15"),
(set(u"©®".encode("MacRoman")), "MacRoman"),
(set(u"εδηιθκμοΡσςυΈί".encode("ISO-8859-7")), "ISO-8859-7"),
)
def guessBytesCharset(bytes, default=None):
r"""
>>> guessBytesCharset("abc")
'ASCII'
>>> guessBytesCharset("\xEF\xBB\xBFabc")
'UTF-8'
>>> guessBytesCharset("abc\xC3\xA9")
'UTF-8'
>>> guessBytesCharset("File written by Adobe Photoshop\xA8 4.0\0")
'MacRoman'
>>> guessBytesCharset("\xE9l\xE9phant")
'ISO-8859-1'
>>> guessBytesCharset("100 \xA4")
'ISO-8859-15'
>>> guessBytesCharset('Word \xb8\xea\xe4\xef\xf3\xe7 - Microsoft Outlook 97 - \xd1\xf5\xe8\xec\xdf\xf3\xe5\xe9\xf2 e-mail')
'ISO-8859-7'
"""
# Check for UTF BOM
for bom_bytes, charset in UTF_BOMS:
if bytes.startswith(bom_bytes):
return charset
# Pure ASCII?
try:
text = unicode(bytes, 'ASCII', 'strict')
return 'ASCII'
except UnicodeDecodeError:
pass
# Valid UTF-8?
try:
text = unicode(bytes, 'UTF-8', 'strict')
return 'UTF-8'
except UnicodeDecodeError:
pass
# Create a set of non-ASCII characters
non_ascii_set = set( byte for byte in bytes if ord(byte) >= 128 )
for characters, charset in CHARSET_CHARACTERS:
if characters.issuperset(non_ascii_set):
return charset
return default
# Initialize _(), gettext() and ngettext() functions
gettext, ngettext = _initGettext()
_ = gettext
-558
View File
@@ -1,558 +0,0 @@
# -*- coding: utf-8 -*-
"""
ISO639-2 standart: the module only contains the dictionary ISO639_2
which maps a language code in three letters (eg. "fre") to a language
name in english (eg. "French").
"""
# ISO-639, the list comes from:
# http://www.loc.gov/standards/iso639-2/php/English_list.php
_ISO639 = (
(u"Abkhazian", "abk", "ab"),
(u"Achinese", "ace", None),
(u"Acoli", "ach", None),
(u"Adangme", "ada", None),
(u"Adygei", "ady", None),
(u"Adyghe", "ady", None),
(u"Afar", "aar", "aa"),
(u"Afrihili", "afh", None),
(u"Afrikaans", "afr", "af"),
(u"Afro-Asiatic (Other)", "afa", None),
(u"Ainu", "ain", None),
(u"Akan", "aka", "ak"),
(u"Akkadian", "akk", None),
(u"Albanian", "alb/sqi", "sq"),
(u"Alemani", "gsw", None),
(u"Aleut", "ale", None),
(u"Algonquian languages", "alg", None),
(u"Altaic (Other)", "tut", None),
(u"Amharic", "amh", "am"),
(u"Angika", "anp", None),
(u"Apache languages", "apa", None),
(u"Arabic", "ara", "ar"),
(u"Aragonese", "arg", "an"),
(u"Aramaic", "arc", None),
(u"Arapaho", "arp", None),
(u"Araucanian", "arn", None),
(u"Arawak", "arw", None),
(u"Armenian", "arm/hye", "hy"),
(u"Aromanian", "rup", None),
(u"Artificial (Other)", "art", None),
(u"Arumanian", "rup", None),
(u"Assamese", "asm", "as"),
(u"Asturian", "ast", None),
(u"Athapascan languages", "ath", None),
(u"Australian languages", "aus", None),
(u"Austronesian (Other)", "map", None),
(u"Avaric", "ava", "av"),
(u"Avestan", "ave", "ae"),
(u"Awadhi", "awa", None),
(u"Aymara", "aym", "ay"),
(u"Azerbaijani", "aze", "az"),
(u"Bable", "ast", None),
(u"Balinese", "ban", None),
(u"Baltic (Other)", "bat", None),
(u"Baluchi", "bal", None),
(u"Bambara", "bam", "bm"),
(u"Bamileke languages", "bai", None),
(u"Banda", "bad", None),
(u"Bantu (Other)", "bnt", None),
(u"Basa", "bas", None),
(u"Bashkir", "bak", "ba"),
(u"Basque", "baq/eus", "eu"),
(u"Batak (Indonesia)", "btk", None),
(u"Beja", "bej", None),
(u"Belarusian", "bel", "be"),
(u"Bemba", "bem", None),
(u"Bengali", "ben", "bn"),
(u"Berber (Other)", "ber", None),
(u"Bhojpuri", "bho", None),
(u"Bihari", "bih", "bh"),
(u"Bikol", "bik", None),
(u"Bilin", "byn", None),
(u"Bini", "bin", None),
(u"Bislama", "bis", "bi"),
(u"Blin", "byn", None),
(u"Bokmål, Norwegian", "nob", "nb"),
(u"Bosnian", "bos", "bs"),
(u"Braj", "bra", None),
(u"Breton", "bre", "br"),
(u"Buginese", "bug", None),
(u"Bulgarian", "bul", "bg"),
(u"Buriat", "bua", None),
(u"Burmese", "bur/mya", "my"),
(u"Caddo", "cad", None),
(u"Carib", "car", None),
(u"Castilian", "spa", "es"),
(u"Catalan", "cat", "ca"),
(u"Caucasian (Other)", "cau", None),
(u"Cebuano", "ceb", None),
(u"Celtic (Other)", "cel", None),
(u"Central American Indian (Other)", "cai", None),
(u"Chagatai", "chg", None),
(u"Chamic languages", "cmc", None),
(u"Chamorro", "cha", "ch"),
(u"Chechen", "che", "ce"),
(u"Cherokee", "chr", None),
(u"Chewa", "nya", "ny"),
(u"Cheyenne", "chy", None),
(u"Chibcha", "chb", None),
(u"Chichewa", "nya", "ny"),
(u"Chinese", "chi/zho", "zh"),
(u"Chinook jargon", "chn", None),
(u"Chipewyan", "chp", None),
(u"Choctaw", "cho", None),
(u"Chuang", "zha", "za"),
(u"Church Slavic", "chu", "cu"),
(u"Church Slavonic", "chu", "cu"),
(u"Chuukese", "chk", None),
(u"Chuvash", "chv", "cv"),
(u"Classical Nepal Bhasa", "nwc", None),
(u"Classical Newari", "nwc", None),
(u"Coptic", "cop", None),
(u"Cornish", "cor", "kw"),
(u"Corsican", "cos", "co"),
(u"Cree", "cre", "cr"),
(u"Creek", "mus", None),
(u"Creoles and pidgins (Other)", "crp", None),
(u"Creoles and pidgins, English based (Other)", "cpe", None),
(u"Creoles and pidgins, French-based (Other)", "cpf", None),
(u"Creoles and pidgins, Portuguese-based (Other)", "cpp", None),
(u"Crimean Tatar", "crh", None),
(u"Crimean Turkish", "crh", None),
(u"Croatian", "scr/hrv", "hr"),
(u"Cushitic (Other)", "cus", None),
(u"Czech", "cze/ces", "cs"),
(u"Dakota", "dak", None),
(u"Danish", "dan", "da"),
(u"Dargwa", "dar", None),
(u"Dayak", "day", None),
(u"Delaware", "del", None),
(u"Dhivehi", "div", "dv"),
(u"Dimili", "zza", None),
(u"Dimli", "zza", None),
(u"Dinka", "din", None),
(u"Divehi", "div", "dv"),
(u"Dogri", "doi", None),
(u"Dogrib", "dgr", None),
(u"Dravidian (Other)", "dra", None),
(u"Duala", "dua", None),
(u"Dutch", "dut/nld", "nl"),
(u"Dutch, Middle (ca.1050-1350)", "dum", None),
(u"Dyula", "dyu", None),
(u"Dzongkha", "dzo", "dz"),
(u"Eastern Frisian", "frs", None),
(u"Efik", "efi", None),
(u"Egyptian (Ancient)", "egy", None),
(u"Ekajuk", "eka", None),
(u"Elamite", "elx", None),
(u"English", "eng", "en"),
(u"English, Middle (1100-1500)", "enm", None),
(u"English, Old (ca.450-1100)", "ang", None),
(u"Erzya", "myv", None),
(u"Esperanto", "epo", "eo"),
(u"Estonian", "est", "et"),
(u"Ewe", "ewe", "ee"),
(u"Ewondo", "ewo", None),
(u"Fang", "fan", None),
(u"Fanti", "fat", None),
(u"Faroese", "fao", "fo"),
(u"Fijian", "fij", "fj"),
(u"Filipino", "fil", None),
(u"Finnish", "fin", "fi"),
(u"Finno-Ugrian (Other)", "fiu", None),
(u"Flemish", "dut/nld", "nl"),
(u"Fon", "fon", None),
(u"French", "fre/fra", "fr"),
(u"French, Middle (ca.1400-1600)", "frm", None),
(u"French, Old (842-ca.1400)", "fro", None),
(u"Friulian", "fur", None),
(u"Fulah", "ful", "ff"),
(u"Ga", "gaa", None),
(u"Gaelic", "gla", "gd"),
(u"Galician", "glg", "gl"),
(u"Ganda", "lug", "lg"),
(u"Gayo", "gay", None),
(u"Gbaya", "gba", None),
(u"Geez", "gez", None),
(u"Georgian", "geo/kat", "ka"),
(u"German", "ger/deu", "de"),
(u"German, Low", "nds", None),
(u"German, Middle High (ca.1050-1500)", "gmh", None),
(u"German, Old High (ca.750-1050)", "goh", None),
(u"Germanic (Other)", "gem", None),
(u"Gikuyu", "kik", "ki"),
(u"Gilbertese", "gil", None),
(u"Gondi", "gon", None),
(u"Gorontalo", "gor", None),
(u"Gothic", "got", None),
(u"Grebo", "grb", None),
(u"Greek, Ancient (to 1453)", "grc", None),
(u"Greek, Modern (1453-)", "gre/ell", "el"),
(u"Greenlandic", "kal", "kl"),
(u"Guarani", "grn", "gn"),
(u"Gujarati", "guj", "gu"),
(u"Gwich´in", "gwi", None),
(u"Haida", "hai", None),
(u"Haitian", "hat", "ht"),
(u"Haitian Creole", "hat", "ht"),
(u"Hausa", "hau", "ha"),
(u"Hawaiian", "haw", None),
(u"Hebrew", "heb", "he"),
(u"Herero", "her", "hz"),
(u"Hiligaynon", "hil", None),
(u"Himachali", "him", None),
(u"Hindi", "hin", "hi"),
(u"Hiri Motu", "hmo", "ho"),
(u"Hittite", "hit", None),
(u"Hmong", "hmn", None),
(u"Hungarian", "hun", "hu"),
(u"Hupa", "hup", None),
(u"Iban", "iba", None),
(u"Icelandic", "ice/isl", "is"),
(u"Ido", "ido", "io"),
(u"Igbo", "ibo", "ig"),
(u"Ijo", "ijo", None),
(u"Iloko", "ilo", None),
(u"Inari Sami", "smn", None),
(u"Indic (Other)", "inc", None),
(u"Indo-European (Other)", "ine", None),
(u"Indonesian", "ind", "id"),
(u"Ingush", "inh", None),
(u"Interlingua", "ina", "ia"),
(u"Interlingue", "ile", "ie"),
(u"Inuktitut", "iku", "iu"),
(u"Inupiaq", "ipk", "ik"),
(u"Iranian (Other)", "ira", None),
(u"Irish", "gle", "ga"),
(u"Irish, Middle (900-1200)", "mga", None),
(u"Irish, Old (to 900)", "sga", None),
(u"Iroquoian languages", "iro", None),
(u"Italian", "ita", "it"),
(u"Japanese", "jpn", "ja"),
(u"Javanese", "jav", "jv"),
(u"Judeo-Arabic", "jrb", None),
(u"Judeo-Persian", "jpr", None),
(u"Kabardian", "kbd", None),
(u"Kabyle", "kab", None),
(u"Kachin", "kac", None),
(u"Kalaallisut", "kal", "kl"),
(u"Kalmyk", "xal", None),
(u"Kamba", "kam", None),
(u"Kannada", "kan", "kn"),
(u"Kanuri", "kau", "kr"),
(u"Kara-Kalpak", "kaa", None),
(u"Karachay-Balkar", "krc", None),
(u"Karelian", "krl", None),
(u"Karen", "kar", None),
(u"Kashmiri", "kas", "ks"),
(u"Kashubian", "csb", None),
(u"Kawi", "kaw", None),
(u"Kazakh", "kaz", "kk"),
(u"Khasi", "kha", None),
(u"Khmer", "khm", "km"),
(u"Khoisan (Other)", "khi", None),
(u"Khotanese", "kho", None),
(u"Kikuyu", "kik", "ki"),
(u"Kimbundu", "kmb", None),
(u"Kinyarwanda", "kin", "rw"),
(u"Kirdki", "zza", None),
(u"Kirghiz", "kir", "ky"),
(u"Kirmanjki", "zza", None),
(u"Klingon", "tlh", None),
(u"Komi", "kom", "kv"),
(u"Kongo", "kon", "kg"),
(u"Konkani", "kok", None),
(u"Korean", "kor", "ko"),
(u"Kosraean", "kos", None),
(u"Kpelle", "kpe", None),
(u"Kru", "kro", None),
(u"Kuanyama", "kua", "kj"),
(u"Kumyk", "kum", None),
(u"Kurdish", "kur", "ku"),
(u"Kurukh", "kru", None),
(u"Kutenai", "kut", None),
(u"Kwanyama", "kua", "kj"),
(u"Ladino", "lad", None),
(u"Lahnda", "lah", None),
(u"Lamba", "lam", None),
(u"Lao", "lao", "lo"),
(u"Latin", "lat", "la"),
(u"Latvian", "lav", "lv"),
(u"Letzeburgesch", "ltz", "lb"),
(u"Lezghian", "lez", None),
(u"Limburgan", "lim", "li"),
(u"Limburger", "lim", "li"),
(u"Limburgish", "lim", "li"),
(u"Lingala", "lin", "ln"),
(u"Lithuanian", "lit", "lt"),
(u"Lojban", "jbo", None),
(u"Low German", "nds", None),
(u"Low Saxon", "nds", None),
(u"Lower Sorbian", "dsb", None),
(u"Lozi", "loz", None),
(u"Luba-Katanga", "lub", "lu"),
(u"Luba-Lulua", "lua", None),
(u"Luiseno", "lui", None),
(u"Lule Sami", "smj", None),
(u"Lunda", "lun", None),
(u"Luo (Kenya and Tanzania)", "luo", None),
(u"Lushai", "lus", None),
(u"Luxembourgish", "ltz", "lb"),
(u"Macedo-Romanian", "rup", None),
(u"Macedonian", "mac/mkd", "mk"),
(u"Madurese", "mad", None),
(u"Magahi", "mag", None),
(u"Maithili", "mai", None),
(u"Makasar", "mak", None),
(u"Malagasy", "mlg", "mg"),
(u"Malay", "may/msa", "ms"),
(u"Malayalam", "mal", "ml"),
(u"Maldivian", "div", "dv"),
(u"Maltese", "mlt", "mt"),
(u"Manchu", "mnc", None),
(u"Mandar", "mdr", None),
(u"Mandingo", "man", None),
(u"Manipuri", "mni", None),
(u"Manobo languages", "mno", None),
(u"Manx", "glv", "gv"),
(u"Maori", "mao/mri", "mi"),
(u"Marathi", "mar", "mr"),
(u"Mari", "chm", None),
(u"Marshallese", "mah", "mh"),
(u"Marwari", "mwr", None),
(u"Masai", "mas", None),
(u"Mayan languages", "myn", None),
(u"Mende", "men", None),
(u"Mi'kmaq", "mic", None),
(u"Micmac", "mic", None),
(u"Minangkabau", "min", None),
(u"Mirandese", "mwl", None),
(u"Miscellaneous languages", "mis", None),
(u"Mohawk", "moh", None),
(u"Moksha", "mdf", None),
(u"Moldavian", "mol", "mo"),
(u"Mon-Khmer (Other)", "mkh", None),
(u"Mongo", "lol", None),
(u"Mongolian", "mon", "mn"),
(u"Mossi", "mos", None),
(u"Multiple languages", "mul", None),
(u"Munda languages", "mun", None),
(u"N'Ko", "nqo", None),
(u"Nahuatl", "nah", None),
(u"Nauru", "nau", "na"),
(u"Navaho", "nav", "nv"),
(u"Navajo", "nav", "nv"),
(u"Ndebele, North", "nde", "nd"),
(u"Ndebele, South", "nbl", "nr"),
(u"Ndonga", "ndo", "ng"),
(u"Neapolitan", "nap", None),
(u"Nepal Bhasa", "new", None),
(u"Nepali", "nep", "ne"),
(u"Newari", "new", None),
(u"Nias", "nia", None),
(u"Niger-Kordofanian (Other)", "nic", None),
(u"Nilo-Saharan (Other)", "ssa", None),
(u"Niuean", "niu", None),
(u"No linguistic content", "zxx", None),
(u"Nogai", "nog", None),
(u"Norse, Old", "non", None),
(u"North American Indian", "nai", None),
(u"North Ndebele", "nde", "nd"),
(u"Northern Frisian", "frr", None),
(u"Northern Sami", "sme", "se"),
(u"Northern Sotho", "nso", None),
(u"Norwegian", "nor", "no"),
(u"Norwegian Bokmål", "nob", "nb"),
(u"Norwegian Nynorsk", "nno", "nn"),
(u"Nubian languages", "nub", None),
(u"Nyamwezi", "nym", None),
(u"Nyanja", "nya", "ny"),
(u"Nyankole", "nyn", None),
(u"Nynorsk, Norwegian", "nno", "nn"),
(u"Nyoro", "nyo", None),
(u"Nzima", "nzi", None),
(u"Occitan (post 1500)", "oci", "oc"),
(u"Oirat", "xal", None),
(u"Ojibwa", "oji", "oj"),
(u"Old Bulgarian", "chu", "cu"),
(u"Old Church Slavonic", "chu", "cu"),
(u"Old Newari", "nwc", None),
(u"Old Slavonic", "chu", "cu"),
(u"Oriya", "ori", "or"),
(u"Oromo", "orm", "om"),
(u"Osage", "osa", None),
(u"Ossetian", "oss", "os"),
(u"Ossetic", "oss", "os"),
(u"Otomian languages", "oto", None),
(u"Pahlavi", "pal", None),
(u"Palauan", "pau", None),
(u"Pali", "pli", "pi"),
(u"Pampanga", "pam", None),
(u"Pangasinan", "pag", None),
(u"Panjabi", "pan", "pa"),
(u"Papiamento", "pap", None),
(u"Papuan (Other)", "paa", None),
(u"Pedi", "nso", None),
(u"Persian", "per/fas", "fa"),
(u"Persian, Old (ca.600-400 B.C.)", "peo", None),
(u"Philippine (Other)", "phi", None),
(u"Phoenician", "phn", None),
(u"Pilipino", "fil", None),
(u"Pohnpeian", "pon", None),
(u"Polish", "pol", "pl"),
(u"Portuguese", "por", "pt"),
(u"Prakrit languages", "pra", None),
(u"Provençal", "oci", "oc"),
(u"Provençal, Old (to 1500)", "pro", None),
(u"Punjabi", "pan", "pa"),
(u"Pushto", "pus", "ps"),
(u"Quechua", "que", "qu"),
(u"Raeto-Romance", "roh", "rm"),
(u"Rajasthani", "raj", None),
(u"Rapanui", "rap", None),
(u"Rarotongan", "rar", None),
(u"Reserved for local use", "qaa/qtz", None),
(u"Romance (Other)", "roa", None),
(u"Romanian", "rum/ron", "ro"),
(u"Romany", "rom", None),
(u"Rundi", "run", "rn"),
(u"Russian", "rus", "ru"),
(u"Salishan languages", "sal", None),
(u"Samaritan Aramaic", "sam", None),
(u"Sami languages (Other)", "smi", None),
(u"Samoan", "smo", "sm"),
(u"Sandawe", "sad", None),
(u"Sango", "sag", "sg"),
(u"Sanskrit", "san", "sa"),
(u"Santali", "sat", None),
(u"Sardinian", "srd", "sc"),
(u"Sasak", "sas", None),
(u"Saxon, Low", "nds", None),
(u"Scots", "sco", None),
(u"Scottish Gaelic", "gla", "gd"),
(u"Selkup", "sel", None),
(u"Semitic (Other)", "sem", None),
(u"Sepedi", "nso", None),
(u"Serbian", "scc/srp", "sr"),
(u"Serer", "srr", None),
(u"Shan", "shn", None),
(u"Shona", "sna", "sn"),
(u"Sichuan Yi", "iii", "ii"),
(u"Sicilian", "scn", None),
(u"Sidamo", "sid", None),
(u"Sign Languages", "sgn", None),
(u"Siksika", "bla", None),
(u"Sindhi", "snd", "sd"),
(u"Sinhala", "sin", "si"),
(u"Sinhalese", "sin", "si"),
(u"Sino-Tibetan (Other)", "sit", None),
(u"Siouan languages", "sio", None),
(u"Skolt Sami", "sms", None),
(u"Slave (Athapascan)", "den", None),
(u"Slavic (Other)", "sla", None),
(u"Slovak", "slo/slk", "sk"),
(u"Slovenian", "slv", "sl"),
(u"Sogdian", "sog", None),
(u"Somali", "som", "so"),
(u"Songhai", "son", None),
(u"Soninke", "snk", None),
(u"Sorbian languages", "wen", None),
(u"Sotho, Northern", "nso", None),
(u"Sotho, Southern", "sot", "st"),
(u"South American Indian (Other)", "sai", None),
(u"South Ndebele", "nbl", "nr"),
(u"Southern Altai", "alt", None),
(u"Southern Sami", "sma", None),
(u"Spanish", "spa", "es"),
(u"Sranan Togo", "srn", None),
(u"Sukuma", "suk", None),
(u"Sumerian", "sux", None),
(u"Sundanese", "sun", "su"),
(u"Susu", "sus", None),
(u"Swahili", "swa", "sw"),
(u"Swati", "ssw", "ss"),
(u"Swedish", "swe", "sv"),
(u"Swiss German", "gsw", None),
(u"Syriac", "syr", None),
(u"Tagalog", "tgl", "tl"),
(u"Tahitian", "tah", "ty"),
(u"Tai (Other)", "tai", None),
(u"Tajik", "tgk", "tg"),
(u"Tamashek", "tmh", None),
(u"Tamil", "tam", "ta"),
(u"Tatar", "tat", "tt"),
(u"Telugu", "tel", "te"),
(u"Tereno", "ter", None),
(u"Tetum", "tet", None),
(u"Thai", "tha", "th"),
(u"Tibetan", "tib/bod", "bo"),
(u"Tigre", "tig", None),
(u"Tigrinya", "tir", "ti"),
(u"Timne", "tem", None),
(u"Tiv", "tiv", None),
(u"tlhIngan-Hol", "tlh", None),
(u"Tlingit", "tli", None),
(u"Tok Pisin", "tpi", None),
(u"Tokelau", "tkl", None),
(u"Tonga (Nyasa)", "tog", None),
(u"Tonga (Tonga Islands)", "ton", "to"),
(u"Tsimshian", "tsi", None),
(u"Tsonga", "tso", "ts"),
(u"Tswana", "tsn", "tn"),
(u"Tumbuka", "tum", None),
(u"Tupi languages", "tup", None),
(u"Turkish", "tur", "tr"),
(u"Turkish, Ottoman (1500-1928)", "ota", None),
(u"Turkmen", "tuk", "tk"),
(u"Tuvalu", "tvl", None),
(u"Tuvinian", "tyv", None),
(u"Twi", "twi", "tw"),
(u"Udmurt", "udm", None),
(u"Ugaritic", "uga", None),
(u"Uighur", "uig", "ug"),
(u"Ukrainian", "ukr", "uk"),
(u"Umbundu", "umb", None),
(u"Undetermined", "und", None),
(u"Upper Sorbian", "hsb", None),
(u"Urdu", "urd", "ur"),
(u"Uyghur", "uig", "ug"),
(u"Uzbek", "uzb", "uz"),
(u"Vai", "vai", None),
(u"Valencian", "cat", "ca"),
(u"Venda", "ven", "ve"),
(u"Vietnamese", "vie", "vi"),
(u"Volapük", "vol", "vo"),
(u"Votic", "vot", None),
(u"Wakashan languages", "wak", None),
(u"Walamo", "wal", None),
(u"Walloon", "wln", "wa"),
(u"Waray", "war", None),
(u"Washo", "was", None),
(u"Welsh", "wel/cym", "cy"),
(u"Western Frisian", "fry", "fy"),
(u"Wolof", "wol", "wo"),
(u"Xhosa", "xho", "xh"),
(u"Yakut", "sah", None),
(u"Yao", "yao", None),
(u"Yapese", "yap", None),
(u"Yiddish", "yid", "yi"),
(u"Yoruba", "yor", "yo"),
(u"Yupik languages", "ypk", None),
(u"Zande", "znd", None),
(u"Zapotec", "zap", None),
(u"Zaza", "zza", None),
(u"Zazaki", "zza", None),
(u"Zenaga", "zen", None),
(u"Zhuang", "zha", "za"),
(u"Zulu", "zul", "zu"),
(u"Zuni", "zun", None),
)
# Bibliographic ISO-639-2 form (eg. "fre" => "French")
ISO639_2 = {}
for line in _ISO639:
for key in line[1].split("/"):
ISO639_2[key] = line[0]
del _ISO639
-23
View File
@@ -1,23 +0,0 @@
from hachoir_core.iso639 import ISO639_2
class Language:
def __init__(self, code):
code = str(code)
if code not in ISO639_2:
raise ValueError("Invalid language code: %r" % code)
self.code = code
def __cmp__(self, other):
if other.__class__ != Language:
return 1
return cmp(self.code, other.code)
def __unicode__(self):
return ISO639_2[self.code]
def __str__(self):
return self.__unicode__()
def __repr__(self):
return "<Language '%s', code=%r>" % (unicode(self), self.code)
-144
View File
@@ -1,144 +0,0 @@
import os, sys, time
import hachoir_core.config as config
from hachoir_core.i18n import _
class Log:
LOG_INFO = 0
LOG_WARN = 1
LOG_ERROR = 2
level_name = {
LOG_WARN: "[warn]",
LOG_ERROR: "[err!]",
LOG_INFO: "[info]"
}
def __init__(self):
self.__buffer = {}
self.__file = None
self.use_print = True
self.use_buffer = False
self.on_new_message = None # Prototype: def func(level, prefix, text, context)
def shutdown(self):
if self.__file:
self._writeIntoFile(_("Stop Hachoir"))
def setFilename(self, filename, append=True):
"""
Use a file to store all messages. The
UTF-8 encoding will be used. Write an informative
message if the file can't be created.
@param filename: C{L{string}}
"""
# Look if file already exists or not
filename = os.path.expanduser(filename)
filename = os.path.realpath(filename)
append = os.access(filename, os.F_OK)
# Create log file (or open it in append mode, if it already exists)
try:
import codecs
if append:
self.__file = codecs.open(filename, "a", "utf-8")
else:
self.__file = codecs.open(filename, "w", "utf-8")
self._writeIntoFile(_("Starting Hachoir"))
except IOError, err:
if err.errno == 2:
self.__file = None
self.info(_("[Log] setFilename(%s) fails: no such file") % filename)
else:
raise
def _writeIntoFile(self, message):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
self.__file.write(u"%s - %s\n" % (timestamp, message))
self.__file.flush()
def newMessage(self, level, text, ctxt=None):
"""
Write a new message : append it in the buffer,
display it to the screen (if needed), and write
it in the log file (if needed).
@param level: Message level.
@type level: C{int}
@param text: Message content.
@type text: C{str}
@param ctxt: The caller instance.
"""
if level < self.LOG_ERROR and config.quiet or \
level <= self.LOG_INFO and not config.verbose:
return
if config.debug:
from hachoir_core.error import getBacktrace
backtrace = getBacktrace(None)
if backtrace:
text += "\n\n" + backtrace
_text = text
if hasattr(ctxt, "_logger"):
_ctxt = ctxt._logger()
if _ctxt is not None:
text = "[%s] %s" % (_ctxt, text)
# Add message to log buffer
if self.use_buffer:
if not self.__buffer.has_key(level):
self.__buffer[level] = [text]
else:
self.__buffer[level].append(text)
# Add prefix
prefix = self.level_name.get(level, "[info]")
# Display on stdout (if used)
if self.use_print:
sys.stdout.flush()
sys.stderr.write("%s %s\n" % (prefix, text))
sys.stderr.flush()
# Write into outfile (if used)
if self.__file:
self._writeIntoFile("%s %s" % (prefix, text))
# Use callback (if used)
if self.on_new_message:
self.on_new_message (level, prefix, _text, ctxt)
def info(self, text):
"""
New informative message.
@type text: C{str}
"""
self.newMessage(Log.LOG_INFO, text)
def warning(self, text):
"""
New warning message.
@type text: C{str}
"""
self.newMessage(Log.LOG_WARN, text)
def error(self, text):
"""
New error message.
@type text: C{str}
"""
self.newMessage(Log.LOG_ERROR, text)
log = Log()
class Logger(object):
def _logger(self):
return "<%s>" % self.__class__.__name__
def info(self, text):
log.newMessage(Log.LOG_INFO, text, self)
def warning(self, text):
log.newMessage(Log.LOG_WARN, text, self)
def error(self, text):
log.newMessage(Log.LOG_ERROR, text, self)
-99
View File
@@ -1,99 +0,0 @@
import gc
#---- Default implementation when resource is missing ----------------------
PAGE_SIZE = 4096
def getMemoryLimit():
"""
Get current memory limit in bytes.
Return None on error.
"""
return None
def setMemoryLimit(max_mem):
"""
Set memory limit in bytes.
Use value 'None' to disable memory limit.
Return True if limit is set, False on error.
"""
return False
def getMemorySize():
"""
Read currenet process memory size: size of available virtual memory.
This value is NOT the real memory usage.
This function only works on Linux (use /proc/self/statm file).
"""
try:
statm = open('/proc/self/statm').readline().split()
except IOError:
return None
return int(statm[0]) * PAGE_SIZE
def clearCaches():
"""
Try to clear all caches: call gc.collect() (Python garbage collector).
"""
gc.collect()
#import re; re.purge()
try:
#---- 'resource' implementation ---------------------------------------------
from resource import getpagesize, getrlimit, setrlimit, RLIMIT_AS
PAGE_SIZE = getpagesize()
def getMemoryLimit():
try:
limit = getrlimit(RLIMIT_AS)[0]
if 0 < limit:
limit *= PAGE_SIZE
return limit
except ValueError:
return None
def setMemoryLimit(max_mem):
if max_mem is None:
max_mem = -1
try:
setrlimit(RLIMIT_AS, (max_mem, -1))
return True
except ValueError:
return False
except ImportError:
pass
def limitedMemory(limit, func, *args, **kw):
"""
Limit memory grow when calling func(*args, **kw):
restrict memory grow to 'limit' bytes.
Use try/except MemoryError to catch the error.
"""
# First step: clear cache to gain memory
clearCaches()
# Get total program size
max_rss = getMemorySize()
if max_rss is not None:
# Get old limit and then set our new memory limit
old_limit = getMemoryLimit()
limit = max_rss + limit
limited = setMemoryLimit(limit)
else:
limited = False
try:
# Call function
return func(*args, **kw)
finally:
# and unset our memory limit
if limited:
setMemoryLimit(old_limit)
# After calling the function: clear all caches
clearCaches()
-31
View File
@@ -1,31 +0,0 @@
from hotshot import Profile
from hotshot.stats import load as loadStats
from os import unlink
def runProfiler(func, args=tuple(), kw={}, verbose=True, nb_func=25, sort_by=('cumulative', 'calls')):
profile_filename = "/tmp/profiler"
prof = Profile(profile_filename)
try:
if verbose:
print "[+] Run profiler"
result = prof.runcall(func, *args, **kw)
prof.close()
if verbose:
print "[+] Stop profiler"
print "[+] Process data..."
stat = loadStats(profile_filename)
if verbose:
print "[+] Strip..."
stat.strip_dirs()
if verbose:
print "[+] Sort data..."
stat.sort_stats(*sort_by)
if verbose:
print
print "[+] Display statistics"
print
stat.print_stats(nb_func)
return result
finally:
unlink(profile_filename)
-11
View File
@@ -1,11 +0,0 @@
from hachoir_core.endian import BIG_ENDIAN, LITTLE_ENDIAN
from hachoir_core.stream.stream import StreamError
from hachoir_core.stream.input import (
InputStreamError,
InputStream, InputIOStream, StringInputStream,
InputSubStream, InputFieldStream,
FragmentedStream, ConcatStream)
from hachoir_core.stream.input_helper import FileInputStream, guessStreamCharset
from hachoir_core.stream.output import (OutputStreamError,
FileOutputStream, StringOutputStream, OutputStream)
-563
View File
@@ -1,563 +0,0 @@
from hachoir_core.endian import BIG_ENDIAN, LITTLE_ENDIAN
from hachoir_core.error import info
from hachoir_core.log import Logger
from hachoir_core.bits import str2long
from hachoir_core.i18n import getTerminalCharset
from hachoir_core.tools import lowerBound
from hachoir_core.i18n import _
from errno import ESPIPE
from weakref import ref as weakref_ref
from hachoir_core.stream import StreamError
class InputStreamError(StreamError):
pass
class ReadStreamError(InputStreamError):
def __init__(self, size, address, got=None):
self.size = size
self.address = address
self.got = got
if self.got is not None:
msg = _("Can't read %u bits at address %u (got %u bits)") % (self.size, self.address, self.got)
else:
msg = _("Can't read %u bits at address %u") % (self.size, self.address)
InputStreamError.__init__(self, msg)
class NullStreamError(InputStreamError):
def __init__(self, source):
self.source = source
msg = _("Input size is nul (source='%s')!") % self.source
InputStreamError.__init__(self, msg)
class FileFromInputStream:
_offset = 0
_from_end = False
def __init__(self, stream):
self.stream = stream
self._setSize(stream.askSize(self))
def _setSize(self, size):
if size is None:
self._size = size
elif size % 8:
raise InputStreamError("Invalid size")
else:
self._size = size // 8
def tell(self):
if self._from_end:
while self._size is None:
self.stream._feed(max(self.stream._current_size << 1, 1 << 16))
self._from_end = False
self._offset += self._size
return self._offset
def seek(self, pos, whence=0):
if whence == 0:
self._from_end = False
self._offset = pos
elif whence == 1:
self._offset += pos
elif whence == 2:
self._from_end = True
self._offset = pos
else:
raise ValueError("seek() second argument must be 0, 1 or 2")
def read(self, size=None):
def read(address, size):
shift, data, missing = self.stream.read(8 * address, 8 * size)
if shift:
raise InputStreamError("TODO: handle non-byte-aligned data")
return data
if self._size or size is not None and not self._from_end:
# We don't want self.tell() to read anything
# and the size must be known if we read until the end.
pos = self.tell()
if size is None or None < self._size < pos + size:
size = self._size - pos
if size <= 0:
return ''
data = read(pos, size)
self._offset += len(data)
return data
elif self._from_end:
# TODO: not tested
max_size = - self._offset
if size is None or max_size < size:
size = max_size
if size <= 0:
return ''
data = '', ''
self._offset = max(0, self.stream._current_size // 8 + self._offset)
self._from_end = False
bs = max(max_size, 1 << 16)
while True:
d = read(self._offset, bs)
data = data[1], d
self._offset += len(d)
if self._size:
bs = self._size - self._offset
if not bs:
data = data[0] + data[1]
d = len(data) - max_size
return data[d:d+size]
else:
# TODO: not tested
data = [ ]
size = 1 << 16
while True:
d = read(self._offset, size)
data.append(d)
self._offset += len(d)
if self._size:
size = self._size - self._offset
if not size:
return ''.join(data)
class InputStream(Logger):
_set_size = None
_current_size = 0
def __init__(self, source=None, size=None, packets=None, **args):
self.source = source
self._size = size # in bits
if size == 0:
raise NullStreamError(source)
self.tags = tuple(args.get("tags", tuple()))
self.packets = packets
def askSize(self, client):
if self._size != self._current_size:
if self._set_size is None:
self._set_size = []
self._set_size.append(weakref_ref(client))
return self._size
def _setSize(self, size=None):
assert self._size is None or self._current_size <= self._size
if self._size != self._current_size:
self._size = self._current_size
if not self._size:
raise NullStreamError(self.source)
if self._set_size:
for client in self._set_size:
client = client()
if client:
client._setSize(self._size)
del self._set_size
size = property(lambda self: self._size, doc="Size of the stream in bits")
checked = property(lambda self: self._size == self._current_size)
def sizeGe(self, size, const=False):
return self._current_size >= size or \
not (None < self._size < size or const or self._feed(size))
def _feed(self, size):
return self.read(size-1,1)[2]
def read(self, address, size):
"""
Read 'size' bits at position 'address' (in bits)
from the beginning of the stream.
"""
raise NotImplementedError
def readBits(self, address, nbits, endian):
assert endian in (BIG_ENDIAN, LITTLE_ENDIAN)
shift, data, missing = self.read(address, nbits)
if missing:
raise ReadStreamError(nbits, address)
value = str2long(data, endian)
if endian is BIG_ENDIAN:
value >>= len(data) * 8 - shift - nbits
else:
value >>= shift
return value & (1 << nbits) - 1
def readInteger(self, address, signed, nbits, endian):
""" Read an integer number """
value = self.readBits(address, nbits, endian)
# Signe number. Example with nbits=8:
# if 128 <= value: value -= 256
if signed and (1 << (nbits-1)) <= value:
value -= (1 << nbits)
return value
def readBytes(self, address, nb_bytes):
shift, data, missing = self.read(address, 8 * nb_bytes)
if shift:
raise InputStreamError("TODO: handle non-byte-aligned data")
if missing:
raise ReadStreamError(8 * nb_bytes, address)
return data
def searchBytesLength(self, needle, include_needle,
start_address=0, end_address=None):
"""
If include_needle is True, add its length to the result.
Returns None is needle can't be found.
"""
pos = self.searchBytes(needle, start_address, end_address)
if pos is None:
return None
length = (pos - start_address) // 8
if include_needle:
length += len(needle)
return length
def searchBytes(self, needle, start_address=0, end_address=None):
"""
Search some bytes in [start_address;end_address[. Addresses must
be aligned to byte. Returns the address of the bytes if found,
None else.
"""
if start_address % 8:
raise InputStreamError("Unable to search bytes with address with bit granularity")
length = len(needle)
size = max(3 * length, 4096)
buffer = ''
if self._size and (end_address is None or self._size < end_address):
end_address = self._size
while True:
if end_address is not None:
todo = (end_address - start_address) >> 3
if todo < size:
if todo <= 0:
return None
size = todo
data = self.readBytes(start_address, size)
if end_address is None and self._size:
end_address = self._size
size = (end_address - start_address) >> 3
assert size > 0
data = data[:size]
start_address += 8 * size
buffer = buffer[len(buffer) - length + 1:] + data
found = buffer.find(needle)
if found >= 0:
return start_address + (found - len(buffer)) * 8
def file(self):
return FileFromInputStream(self)
class InputPipe(object):
"""
InputPipe makes input streams seekable by caching a certain
amount of data. The memory usage may be unlimited in worst cases.
A function (set_size) is called when the size of the stream is known.
InputPipe sees the input stream as an array of blocks of
size = (2 ^ self.buffer_size) and self.buffers maps to this array.
It also maintains a circular ordered list of non-discarded blocks,
sorted by access time.
Each element of self.buffers is an array of 3 elements:
* self.buffers[i][0] is the data.
len(self.buffers[i][0]) == 1 << self.buffer_size
(except at the end: the length may be smaller)
* self.buffers[i][1] is the index of a more recently used block
* self.buffers[i][2] is the opposite of self.buffers[1],
in order to have a double-linked list.
For any discarded block, self.buffers[i] = None
self.last is the index of the most recently accessed block.
self.first is the first (= smallest index) non-discarded block.
How InputPipe discards blocks:
* Just before returning from the read method.
* Only if there are more than self.buffer_nb_min blocks in memory.
* While self.buffers[self.first] is that least recently used block.
Property: There is no hole in self.buffers, except at the beginning.
"""
buffer_nb_min = 256
buffer_size = 16
last = None
size = None
def __init__(self, input, set_size=None):
self._input = input
self.first = self.address = 0
self.buffers = []
self.set_size = set_size
current_size = property(lambda self: len(self.buffers) << self.buffer_size)
def _append(self, data):
if self.last is None:
self.last = next = prev = 0
else:
prev = self.last
last = self.buffers[prev]
next = last[1]
self.last = self.buffers[next][2] = last[1] = len(self.buffers)
self.buffers.append([ data, next, prev ])
def _get(self, index):
if index >= len(self.buffers):
return ''
buf = self.buffers[index]
if buf is None:
raise InputStreamError(_("Error: Buffers too small. Can't seek backward."))
if self.last != index:
next = buf[1]
prev = buf[2]
self.buffers[next][2] = prev
self.buffers[prev][1] = next
first = self.buffers[self.last][1]
buf[1] = first
buf[2] = self.last
self.buffers[first][2] = index
self.buffers[self.last][1] = index
self.last = index
return buf[0]
def _flush(self):
lim = len(self.buffers) - self.buffer_nb_min
while self.first < lim:
buf = self.buffers[self.first]
if buf[2] != self.last:
break
info("Discarding buffer %u." % self.first)
self.buffers[self.last][1] = buf[1]
self.buffers[buf[1]][2] = self.last
self.buffers[self.first] = None
self.first += 1
def seek(self, address):
assert 0 <= address
self.address = address
def read(self, size):
end = self.address + size
for i in xrange(len(self.buffers), (end >> self.buffer_size) + 1):
data = self._input.read(1 << self.buffer_size)
if len(data) < 1 << self.buffer_size:
self.size = (len(self.buffers) << self.buffer_size) + len(data)
if self.set_size:
self.set_size(self.size)
if data:
self._append(data)
break
self._append(data)
block, offset = divmod(self.address, 1 << self.buffer_size)
data = ''.join(self._get(index)
for index in xrange(block, (end - 1 >> self.buffer_size) + 1)
)[offset:offset+size]
self._flush()
self.address += len(data)
return data
class InputIOStream(InputStream):
def __init__(self, input, size=None, **args):
if not hasattr(input, "seek"):
if size is None:
input = InputPipe(input, self._setSize)
else:
input = InputPipe(input)
elif size is None:
try:
input.seek(0, 2)
size = input.tell() * 8
except IOError, err:
if err.errno == ESPIPE:
input = InputPipe(input, self._setSize)
else:
charset = getTerminalCharset()
errmsg = unicode(str(err), charset)
source = args.get("source", "<inputio:%r>" % input)
raise InputStreamError(_("Unable to get size of %s: %s") % (source, errmsg))
self._input = input
InputStream.__init__(self, size=size, **args)
def __current_size(self):
if self._size:
return self._size
if self._input.size:
return 8 * self._input.size
return 8 * self._input.current_size
_current_size = property(__current_size)
def read(self, address, size):
assert size > 0
_size = self._size
address, shift = divmod(address, 8)
self._input.seek(address)
size = (size + shift + 7) >> 3
data = self._input.read(size)
got = len(data)
missing = size != got
if missing and _size == self._size:
raise ReadStreamError(8 * size, 8 * address, 8 * got)
return shift, data, missing
def file(self):
if hasattr(self._input, "fileno"):
from os import dup, fdopen
new_fd = dup(self._input.fileno())
new_file = fdopen(new_fd, "r")
new_file.seek(0)
return new_file
return InputStream.file(self)
class StringInputStream(InputStream):
def __init__(self, data, source="<string>", **args):
self.data = data
InputStream.__init__(self, source=source, size=8*len(data), **args)
self._current_size = self._size
def read(self, address, size):
address, shift = divmod(address, 8)
size = (size + shift + 7) >> 3
data = self.data[address:address+size]
got = len(data)
if got != size:
raise ReadStreamError(8 * size, 8 * address, 8 * got)
return shift, data, False
class InputSubStream(InputStream):
def __init__(self, stream, offset, size=None, source=None, **args):
if offset is None:
offset = 0
if size is None and stream.size is not None:
size = stream.size - offset
if None < size <= 0:
raise ValueError("InputSubStream: offset is outside input stream")
self.stream = stream
self._offset = offset
if source is None:
source = "<substream input=%s offset=%s size=%s>" % (stream.source, offset, size)
InputStream.__init__(self, source=source, size=size, **args)
self.stream.askSize(self)
_current_size = property(lambda self: min(self._size, max(0, self.stream._current_size - self._offset)))
def read(self, address, size):
return self.stream.read(self._offset + address, size)
def InputFieldStream(field, **args):
if not field.parent:
return field.stream
stream = field.parent.stream
args["size"] = field.size
args.setdefault("source", stream.source + field.path)
return InputSubStream(stream, field.absolute_address, **args)
class FragmentedStream(InputStream):
def __init__(self, field, **args):
self.stream = field.parent.stream
data = field.getData()
self.fragments = [ (0, data.absolute_address, data.size) ]
self.next = field.next
args.setdefault("source", "%s%s" % (self.stream.source, field.path))
InputStream.__init__(self, **args)
if not self.next:
self._current_size = data.size
self._setSize()
def _feed(self, end):
if self._current_size < end:
if self.checked:
raise ReadStreamError(end - self._size, self._size)
a, fa, fs = self.fragments[-1]
while self.stream.sizeGe(fa + min(fs, end - a)):
a += fs
f = self.next
if a >= end:
self._current_size = end
if a == end and not f:
self._setSize()
return False
if f:
self.next = f.next
f = f.getData()
if not f:
self._current_size = a
self._setSize()
return True
fa = f.absolute_address
fs = f.size
self.fragments += [ (a, fa, fs) ]
self._current_size = a + max(0, self.stream.size - fa)
self._setSize()
return True
return False
def read(self, address, size):
assert size > 0
missing = self._feed(address + size)
if missing:
size = self._size - address
if size <= 0:
return 0, '', True
d = []
i = lowerBound(self.fragments, lambda x: x[0] <= address)
a, fa, fs = self.fragments[i-1]
a -= address
fa -= a
fs += a
s = None
while True:
n = min(fs, size)
u, v, w = self.stream.read(fa, n)
assert not w
if s is None:
s = u
else:
assert not u
d += [ v ]
size -= n
if not size:
return s, ''.join(d), missing
a, fa, fs = self.fragments[i]
i += 1
class ConcatStream(InputStream):
# TODO: concatene any number of any type of stream
def __init__(self, streams, **args):
if len(streams) > 2 or not streams[0].checked:
raise NotImplementedError
self.__size0 = streams[0].size
size1 = streams[1].askSize(self)
if size1 is not None:
args["size"] = self.__size0 + size1
self.__streams = streams
InputStream.__init__(self, **args)
_current_size = property(lambda self: self.__size0 + self.__streams[1]._current_size)
def read(self, address, size):
_size = self._size
s = self.__size0 - address
shift, data, missing = None, '', False
if s > 0:
s = min(size, s)
shift, data, w = self.__streams[0].read(address, s)
assert not w
a, s = 0, size - s
else:
a, s = -s, size
if s:
u, v, missing = self.__streams[1].read(a, s)
if missing and _size == self._size:
raise ReadStreamError(s, a)
if shift is None:
shift = u
else:
assert not u
data += v
return shift, data, missing
-38
View File
@@ -1,38 +0,0 @@
from hachoir_core.i18n import getTerminalCharset, guessBytesCharset, _
from hachoir_core.stream import InputIOStream, InputSubStream, InputStreamError
def FileInputStream(filename, real_filename=None, **args):
"""
Create an input stream of a file. filename must be unicode.
real_filename is an optional argument used to specify the real filename,
its type can be 'str' or 'unicode'. Use real_filename when you are
not able to convert filename to real unicode string (ie. you have to
use unicode(name, 'replace') or unicode(name, 'ignore')).
"""
assert isinstance(filename, unicode)
if not real_filename:
real_filename = filename
try:
inputio = open(real_filename, 'rb')
except IOError, err:
charset = getTerminalCharset()
errmsg = unicode(str(err), charset)
raise InputStreamError(_("Unable to open file %s: %s") % (filename, errmsg))
source = "file:" + filename
offset = args.pop("offset", 0)
size = args.pop("size", None)
if offset or size:
if size:
size = 8 * size
stream = InputIOStream(inputio, source=source, **args)
return InputSubStream(stream, 8 * offset, size, **args)
else:
args.setdefault("tags",[]).append(("filename", filename))
return InputIOStream(inputio, source=source, **args)
def guessStreamCharset(stream, address, size, default=None):
size = min(size, 1024*8)
bytes = stream.readBytes(address, size//8)
return guessBytesCharset(bytes, default)
-173
View File
@@ -1,173 +0,0 @@
from cStringIO import StringIO
from hachoir_core.endian import BIG_ENDIAN
from hachoir_core.bits import long2raw
from hachoir_core.stream import StreamError
from errno import EBADF
MAX_READ_NBYTES = 2 ** 16
class OutputStreamError(StreamError):
pass
class OutputStream(object):
def __init__(self, output, filename=None):
self._output = output
self._filename = filename
self._bit_pos = 0
self._byte = 0
def _getFilename(self):
return self._filename
filename = property(_getFilename)
def writeBit(self, state, endian):
if self._bit_pos == 7:
self._bit_pos = 0
if state:
if endian is BIG_ENDIAN:
self._byte |= 1
else:
self._byte |= 128
self._output.write(chr(self._byte))
self._byte = 0
else:
if state:
if endian is BIG_ENDIAN:
self._byte |= (1 << self._bit_pos)
else:
self._byte |= (1 << (7-self._bit_pos))
self._bit_pos += 1
def writeBits(self, count, value, endian):
assert 0 <= value < 2**count
# Feed bits to align to byte address
if self._bit_pos != 0:
n = 8 - self._bit_pos
if n <= count:
count -= n
if endian is BIG_ENDIAN:
self._byte |= (value >> count)
value &= ((1 << count) - 1)
else:
self._byte |= (value & ((1 << n)-1)) << self._bit_pos
value >>= n
self._output.write(chr(self._byte))
self._bit_pos = 0
self._byte = 0
else:
if endian is BIG_ENDIAN:
self._byte |= (value << (8-self._bit_pos-count))
else:
self._byte |= (value << self._bit_pos)
self._bit_pos += count
return
# Write byte per byte
while 8 <= count:
count -= 8
if endian is BIG_ENDIAN:
byte = (value >> count)
value &= ((1 << count) - 1)
else:
byte = (value & 0xFF)
value >>= 8
self._output.write(chr(byte))
# Keep last bits
assert 0 <= count < 8
self._bit_pos = count
if 0 < count:
assert 0 <= value < 2**count
if endian is BIG_ENDIAN:
self._byte = value << (8-count)
else:
self._byte = value
else:
assert value == 0
self._byte = 0
def writeInteger(self, value, signed, size_byte, endian):
if signed:
value += 1 << (size_byte*8 - 1)
raw = long2raw(value, endian, size_byte)
self.writeBytes(raw)
def copyBitsFrom(self, input, address, nb_bits, endian):
if (nb_bits % 8) == 0:
self.copyBytesFrom(input, address, nb_bits/8)
else:
# Arbitrary limit (because we should use a buffer, like copyBytesFrom(),
# but with endianess problem
assert nb_bits <= 128
data = input.readBits(address, nb_bits, endian)
self.writeBits(nb_bits, data, endian)
def copyBytesFrom(self, input, address, nb_bytes):
if (address % 8):
raise OutputStreamError("Unable to copy bytes with address with bit granularity")
buffer_size = 1 << 12 # 8192 (8 KB)
while 0 < nb_bytes:
# Compute buffer size
if nb_bytes < buffer_size:
buffer_size = nb_bytes
# Read
data = input.readBytes(address, buffer_size)
# Write
self.writeBytes(data)
# Move address
address += buffer_size*8
nb_bytes -= buffer_size
def writeBytes(self, bytes):
if self._bit_pos != 0:
raise NotImplementedError()
self._output.write(bytes)
def readBytes(self, address, nbytes):
"""
Read bytes from the stream at specified address (in bits).
Address have to be a multiple of 8.
nbytes have to in 1..MAX_READ_NBYTES (64 KB).
This method is only supported for StringOuputStream (not on
FileOutputStream).
Return read bytes as byte string.
"""
assert (address % 8) == 0
assert (1 <= nbytes <= MAX_READ_NBYTES)
self._output.flush()
oldpos = self._output.tell()
try:
self._output.seek(0)
try:
return self._output.read(nbytes)
except IOError, err:
if err[0] == EBADF:
raise OutputStreamError("Stream doesn't support read() operation")
finally:
self._output.seek(oldpos)
def StringOutputStream():
"""
Create an output stream into a string.
"""
data = StringIO()
return OutputStream(data)
def FileOutputStream(filename, real_filename=None):
"""
Create an output stream into file with given name.
Filename have to be unicode, whereas (optional) real_filename can be str.
"""
assert isinstance(filename, unicode)
if not real_filename:
real_filename = filename
output = open(real_filename, 'wb')
return OutputStream(output, filename=filename)
-5
View File
@@ -1,5 +0,0 @@
from hachoir_core.error import HachoirError
class StreamError(HachoirError):
pass
-60
View File
@@ -1,60 +0,0 @@
"""
Utilities used to convert a field to human classic reprentation of data.
"""
from hachoir_core.tools import (
humanDuration, humanFilesize, alignValue,
durationWin64 as doDurationWin64,
deprecated)
from types import FunctionType, MethodType
from hachoir_core.field import Field
def textHandler(field, handler):
assert isinstance(handler, (FunctionType, MethodType))
assert issubclass(field.__class__, Field)
field.createDisplay = lambda: handler(field)
return field
def displayHandler(field, handler):
assert isinstance(handler, (FunctionType, MethodType))
assert issubclass(field.__class__, Field)
field.createDisplay = lambda: handler(field.value)
return field
@deprecated("Use TimedeltaWin64 field type")
def durationWin64(field):
"""
Convert Windows 64-bit duration to string. The timestamp format is
a 64-bit number: number of 100ns. See also timestampWin64().
>>> durationWin64(type("", (), dict(value=2146280000, size=64)))
u'3 min 34 sec 628 ms'
>>> durationWin64(type("", (), dict(value=(1 << 64)-1, size=64)))
u'58494 years 88 days 5 hours'
"""
assert hasattr(field, "value") and hasattr(field, "size")
assert field.size == 64
delta = doDurationWin64(field.value)
return humanDuration(delta)
def filesizeHandler(field):
"""
Format field value using humanFilesize()
"""
return displayHandler(field, humanFilesize)
def hexadecimal(field):
"""
Convert an integer to hexadecimal in lower case. Returns unicode string.
>>> hexadecimal(type("", (), dict(value=412, size=16)))
u'0x019c'
>>> hexadecimal(type("", (), dict(value=0, size=32)))
u'0x00000000'
"""
assert hasattr(field, "value") and hasattr(field, "size")
size = field.size
padding = alignValue(size, 4) // 4
pattern = u"0x%%0%ux" % padding
return pattern % field.value
-76
View File
@@ -1,76 +0,0 @@
"""
limitedTime(): set a timeout in seconds when calling a function,
raise a Timeout error if time exceed.
"""
from math import ceil
IMPLEMENTATION = None
class Timeout(RuntimeError):
"""
Timeout error, inherits from RuntimeError
"""
pass
def signalHandler(signum, frame):
"""
Signal handler to catch timeout signal: raise Timeout exception.
"""
raise Timeout("Timeout exceed!")
def limitedTime(second, func, *args, **kw):
"""
Call func(*args, **kw) with a timeout of second seconds.
"""
return func(*args, **kw)
def fixTimeout(second):
"""
Fix timeout value: convert to integer with a minimum of 1 second
"""
if isinstance(second, float):
second = int(ceil(second))
assert isinstance(second, (int, long))
return max(second, 1)
if not IMPLEMENTATION:
try:
from signal import signal, alarm, SIGALRM
# signal.alarm() implementation
def limitedTime(second, func, *args, **kw):
second = fixTimeout(second)
old_alarm = signal(SIGALRM, signalHandler)
try:
alarm(second)
return func(*args, **kw)
finally:
alarm(0)
signal(SIGALRM, old_alarm)
IMPLEMENTATION = "signal.alarm()"
except ImportError:
pass
if not IMPLEMENTATION:
try:
from signal import signal, SIGXCPU
from resource import getrlimit, setrlimit, RLIMIT_CPU
# resource.setrlimit(RLIMIT_CPU) implementation
# "Bug": timeout is 'CPU' time so sleep() are not part of the timeout
def limitedTime(second, func, *args, **kw):
second = fixTimeout(second)
old_alarm = signal(SIGXCPU, signalHandler)
current = getrlimit(RLIMIT_CPU)
try:
setrlimit(RLIMIT_CPU, (second, current[1]))
return func(*args, **kw)
finally:
setrlimit(RLIMIT_CPU, current)
signal(SIGXCPU, old_alarm)
IMPLEMENTATION = "resource.setrlimit(RLIMIT_CPU)"
except ImportError:
pass
-582
View File
@@ -1,582 +0,0 @@
# -*- coding: utf-8 -*-
"""
Various utilities.
"""
from hachoir_core.i18n import _, ngettext
import re
import stat
from datetime import datetime, timedelta, MAXYEAR
from warnings import warn
def deprecated(comment=None):
"""
This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emmitted
when the function is used.
Examples: ::
@deprecated
def oldfunc(): ...
@deprecated("use newfunc()!")
def oldfunc2(): ...
Code from: http://code.activestate.com/recipes/391367/
"""
def _deprecated(func):
def newFunc(*args, **kwargs):
message = "Call to deprecated function %s" % func.__name__
if comment:
message += ": " + comment
warn(message, category=DeprecationWarning, stacklevel=2)
return func(*args, **kwargs)
newFunc.__name__ = func.__name__
newFunc.__doc__ = func.__doc__
newFunc.__dict__.update(func.__dict__)
return newFunc
return _deprecated
def paddingSize(value, align):
"""
Compute size of a padding field.
>>> paddingSize(31, 4)
1
>>> paddingSize(32, 4)
0
>>> paddingSize(33, 4)
3
Note: (value + paddingSize(value, align)) == alignValue(value, align)
"""
if value % align != 0:
return align - (value % align)
else:
return 0
def alignValue(value, align):
"""
Align a value to next 'align' multiple.
>>> alignValue(31, 4)
32
>>> alignValue(32, 4)
32
>>> alignValue(33, 4)
36
Note: alignValue(value, align) == (value + paddingSize(value, align))
"""
if value % align != 0:
return value + align - (value % align)
else:
return value
def timedelta2seconds(delta):
"""
Convert a datetime.timedelta() objet to a number of second
(floatting point number).
>>> timedelta2seconds(timedelta(seconds=2, microseconds=40000))
2.04
>>> timedelta2seconds(timedelta(minutes=1, milliseconds=250))
60.25
"""
return delta.microseconds / 1000000.0 \
+ delta.seconds + delta.days * 60*60*24
def humanDurationNanosec(nsec):
"""
Convert a duration in nanosecond to human natural representation.
Returns an unicode string.
>>> humanDurationNanosec(60417893)
u'60.42 ms'
"""
# Nano second
if nsec < 1000:
return u"%u nsec" % nsec
# Micro seconds
usec, nsec = divmod(nsec, 1000)
if usec < 1000:
return u"%.2f usec" % (usec+float(nsec)/1000)
# Milli seconds
msec, usec = divmod(usec, 1000)
if msec < 1000:
return u"%.2f ms" % (msec + float(usec)/1000)
return humanDuration(msec)
def humanDuration(delta):
"""
Convert a duration in millisecond to human natural representation.
Returns an unicode string.
>>> humanDuration(0)
u'0 ms'
>>> humanDuration(213)
u'213 ms'
>>> humanDuration(4213)
u'4 sec 213 ms'
>>> humanDuration(6402309)
u'1 hour 46 min 42 sec'
"""
if not isinstance(delta, timedelta):
delta = timedelta(microseconds=delta*1000)
# Milliseconds
text = []
if 1000 <= delta.microseconds:
text.append(u"%u ms" % (delta.microseconds//1000))
# Seconds
minutes, seconds = divmod(delta.seconds, 60)
hours, minutes = divmod(minutes, 60)
if seconds:
text.append(u"%u sec" % seconds)
if minutes:
text.append(u"%u min" % minutes)
if hours:
text.append(ngettext("%u hour", "%u hours", hours) % hours)
# Days
years, days = divmod(delta.days, 365)
if days:
text.append(ngettext("%u day", "%u days", days) % days)
if years:
text.append(ngettext("%u year", "%u years", years) % years)
if 3 < len(text):
text = text[-3:]
elif not text:
return u"0 ms"
return u" ".join(reversed(text))
def humanFilesize(size):
"""
Convert a file size in byte to human natural representation.
It uses the values: 1 KB is 1024 bytes, 1 MB is 1024 KB, etc.
The result is an unicode string.
>>> humanFilesize(1)
u'1 byte'
>>> humanFilesize(790)
u'790 bytes'
>>> humanFilesize(256960)
u'250.9 KB'
"""
if size < 10000:
return ngettext("%u byte", "%u bytes", size) % size
units = [_("KB"), _("MB"), _("GB"), _("TB")]
size = float(size)
divisor = 1024
for unit in units:
size = size / divisor
if size < divisor:
return "%.1f %s" % (size, unit)
return "%u %s" % (size, unit)
def humanBitSize(size):
"""
Convert a size in bit to human classic representation.
It uses the values: 1 Kbit is 1000 bits, 1 Mbit is 1000 Kbit, etc.
The result is an unicode string.
>>> humanBitSize(1)
u'1 bit'
>>> humanBitSize(790)
u'790 bits'
>>> humanBitSize(256960)
u'257.0 Kbit'
"""
divisor = 1000
if size < divisor:
return ngettext("%u bit", "%u bits", size) % size
units = [u"Kbit", u"Mbit", u"Gbit", u"Tbit"]
size = float(size)
for unit in units:
size = size / divisor
if size < divisor:
return "%.1f %s" % (size, unit)
return u"%u %s" % (size, unit)
def humanBitRate(size):
"""
Convert a bit rate to human classic representation. It uses humanBitSize()
to convert size into human reprensation. The result is an unicode string.
>>> humanBitRate(790)
u'790 bits/sec'
>>> humanBitRate(256960)
u'257.0 Kbit/sec'
"""
return "".join((humanBitSize(size), "/sec"))
def humanFrequency(hertz):
"""
Convert a frequency in hertz to human classic representation.
It uses the values: 1 KHz is 1000 Hz, 1 MHz is 1000 KMhz, etc.
The result is an unicode string.
>>> humanFrequency(790)
u'790 Hz'
>>> humanFrequency(629469)
u'629.5 kHz'
"""
divisor = 1000
if hertz < divisor:
return u"%u Hz" % hertz
units = [u"kHz", u"MHz", u"GHz", u"THz"]
hertz = float(hertz)
for unit in units:
hertz = hertz / divisor
if hertz < divisor:
return u"%.1f %s" % (hertz, unit)
return u"%s %s" % (hertz, unit)
regex_control_code = re.compile(r"([\x00-\x1f\x7f])")
controlchars = tuple({
# Don't use "\0", because "\0"+"0"+"1" = "\001" = "\1" (1 character)
# Same rease to not use octal syntax ("\1")
ord("\n"): r"\n",
ord("\r"): r"\r",
ord("\t"): r"\t",
ord("\a"): r"\a",
ord("\b"): r"\b",
}.get(code, '\\x%02x' % code)
for code in xrange(128)
)
def makePrintable(data, charset, quote=None, to_unicode=False, smart=True):
r"""
Prepare a string to make it printable in the specified charset.
It escapes control characters. Characters with code bigger than 127
are escaped if data type is 'str' or if charset is "ASCII".
Examples with Unicode:
>>> aged = unicode("âgé", "UTF-8")
>>> repr(aged) # text type is 'unicode'
"u'\\xe2g\\xe9'"
>>> makePrintable("abc\0", "UTF-8")
'abc\\0'
>>> makePrintable(aged, "latin1")
'\xe2g\xe9'
>>> makePrintable(aged, "latin1", quote='"')
'"\xe2g\xe9"'
Examples with string encoded in latin1:
>>> aged_latin = unicode("âgé", "UTF-8").encode("latin1")
>>> repr(aged_latin) # text type is 'str'
"'\\xe2g\\xe9'"
>>> makePrintable(aged_latin, "latin1")
'\\xe2g\\xe9'
>>> makePrintable("", "latin1")
''
>>> makePrintable("a", "latin1", quote='"')
'"a"'
>>> makePrintable("", "latin1", quote='"')
'(empty)'
>>> makePrintable("abc", "latin1", quote="'")
"'abc'"
Control codes:
>>> makePrintable("\0\x03\x0a\x10 \x7f", "latin1")
'\\0\\3\\n\\x10 \\x7f'
Quote character may also be escaped (only ' and "):
>>> print makePrintable("a\"b", "latin-1", quote='"')
"a\"b"
>>> print makePrintable("a\"b", "latin-1", quote="'")
'a"b'
>>> print makePrintable("a'b", "latin-1", quote="'")
'a\'b'
"""
if data:
if not isinstance(data, unicode):
data = unicode(data, "ISO-8859-1")
charset = "ASCII"
data = regex_control_code.sub(
lambda regs: controlchars[ord(regs.group(1))], data)
if quote:
if quote in "\"'":
data = data.replace(quote, '\\' + quote)
data = ''.join((quote, data, quote))
elif quote:
data = "(empty)"
data = data.encode(charset, "backslashreplace")
if smart:
# Replace \x00\x01 by \0\1
data = re.sub(r"\\x0([0-7])(?=[^0-7]|$)", r"\\\1", data)
if to_unicode:
data = unicode(data, charset)
return data
def makeUnicode(text):
r"""
Convert text to printable Unicode string. For byte string (type 'str'),
use charset ISO-8859-1 for the conversion to Unicode
>>> makeUnicode(u'abc\0d')
u'abc\\0d'
>>> makeUnicode('a\xe9')
u'a\xe9'
"""
if isinstance(text, str):
text = unicode(text, "ISO-8859-1")
elif not isinstance(text, unicode):
text = unicode(text)
text = regex_control_code.sub(
lambda regs: controlchars[ord(regs.group(1))], text)
text = re.sub(r"\\x0([0-7])(?=[^0-7]|$)", r"\\\1", text)
return text
def binarySearch(seq, cmp_func):
"""
Search a value in a sequence using binary search. Returns index of the
value, or None if the value doesn't exist.
'seq' have to be sorted in ascending order according to the
comparaison function ;
'cmp_func', prototype func(x), is the compare function:
- Return strictly positive value if we have to search forward ;
- Return strictly negative value if we have to search backward ;
- Otherwise (zero) we got the value.
>>> # Search number 5 (search forward)
... binarySearch([0, 4, 5, 10], lambda x: 5-x)
2
>>> # Backward search
... binarySearch([10, 5, 4, 0], lambda x: x-5)
1
"""
lower = 0
upper = len(seq)
while lower < upper:
index = (lower + upper) >> 1
diff = cmp_func(seq[index])
if diff < 0:
upper = index
elif diff > 0:
lower = index + 1
else:
return index
return None
def lowerBound(seq, cmp_func):
f = 0
l = len(seq)
while l > 0:
h = l >> 1
m = f + h
if cmp_func(seq[m]):
f = m
f += 1
l -= h + 1
else:
l = h
return f
def humanUnixAttributes(mode):
"""
Convert a Unix file attributes (or "file mode") to an unicode string.
Original source code:
http://cvs.savannah.gnu.org/viewcvs/coreutils/lib/filemode.c?root=coreutils
>>> humanUnixAttributes(0644)
u'-rw-r--r-- (644)'
>>> humanUnixAttributes(02755)
u'-rwxr-sr-x (2755)'
"""
def ftypelet(mode):
if stat.S_ISREG (mode) or not stat.S_IFMT(mode):
return '-'
if stat.S_ISBLK (mode): return 'b'
if stat.S_ISCHR (mode): return 'c'
if stat.S_ISDIR (mode): return 'd'
if stat.S_ISFIFO(mode): return 'p'
if stat.S_ISLNK (mode): return 'l'
if stat.S_ISSOCK(mode): return 's'
return '?'
chars = [ ftypelet(mode), 'r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x' ]
for i in xrange(1, 10):
if not mode & 1 << 9 - i:
chars[i] = '-'
if mode & stat.S_ISUID:
if chars[3] != 'x':
chars[3] = 'S'
else:
chars[3] = 's'
if mode & stat.S_ISGID:
if chars[6] != 'x':
chars[6] = 'S'
else:
chars[6] = 's'
if mode & stat.S_ISVTX:
if chars[9] != 'x':
chars[9] = 'T'
else:
chars[9] = 't'
return u"%s (%o)" % (''.join(chars), mode)
def createDict(data, index):
"""
Create a new dictionnay from dictionnary key=>values:
just keep value number 'index' from all values.
>>> data={10: ("dix", 100, "a"), 20: ("vingt", 200, "b")}
>>> createDict(data, 0)
{10: 'dix', 20: 'vingt'}
>>> createDict(data, 2)
{10: 'a', 20: 'b'}
"""
return dict( (key,values[index]) for key, values in data.iteritems() )
# Start of UNIX timestamp (Epoch): 1st January 1970 at 00:00
UNIX_TIMESTAMP_T0 = datetime(1970, 1, 1)
def timestampUNIX(value):
"""
Convert an UNIX (32-bit) timestamp to datetime object. Timestamp value
is the number of seconds since the 1st January 1970 at 00:00. Maximum
value is 2147483647: 19 january 2038 at 03:14:07.
May raise ValueError for invalid value: value have to be in 0..2147483647.
>>> timestampUNIX(0)
datetime.datetime(1970, 1, 1, 0, 0)
>>> timestampUNIX(1154175644)
datetime.datetime(2006, 7, 29, 12, 20, 44)
>>> timestampUNIX(1154175644.37)
datetime.datetime(2006, 7, 29, 12, 20, 44, 370000)
>>> timestampUNIX(2147483647)
datetime.datetime(2038, 1, 19, 3, 14, 7)
"""
if not isinstance(value, (float, int, long)):
raise TypeError("timestampUNIX(): an integer or float is required")
if not(0 <= value <= 2147483647):
raise ValueError("timestampUNIX(): value have to be in 0..2147483647")
return UNIX_TIMESTAMP_T0 + timedelta(seconds=value)
# Start of Macintosh timestamp: 1st January 1904 at 00:00
MAC_TIMESTAMP_T0 = datetime(1904, 1, 1)
def timestampMac32(value):
"""
Convert an Mac (32-bit) timestamp to string. The format is the number
of seconds since the 1st January 1904 (to 2040). Returns unicode string.
>>> timestampMac32(0)
datetime.datetime(1904, 1, 1, 0, 0)
>>> timestampMac32(2843043290)
datetime.datetime(1994, 2, 2, 14, 14, 50)
"""
if not isinstance(value, (float, int, long)):
raise TypeError("an integer or float is required")
if not(0 <= value <= 4294967295):
return _("invalid Mac timestamp (%s)") % value
return MAC_TIMESTAMP_T0 + timedelta(seconds=value)
def durationWin64(value):
"""
Convert Windows 64-bit duration to string. The timestamp format is
a 64-bit number: number of 100ns. See also timestampWin64().
>>> str(durationWin64(1072580000))
'0:01:47.258000'
>>> str(durationWin64(2146280000))
'0:03:34.628000'
"""
if not isinstance(value, (float, int, long)):
raise TypeError("an integer or float is required")
if value < 0:
raise ValueError("value have to be a positive or nul integer")
return timedelta(microseconds=value/10)
# Start of 64-bit Windows timestamp: 1st January 1600 at 00:00
WIN64_TIMESTAMP_T0 = datetime(1601, 1, 1, 0, 0, 0)
def timestampWin64(value):
"""
Convert Windows 64-bit timestamp to string. The timestamp format is
a 64-bit number which represents number of 100ns since the
1st January 1601 at 00:00. Result is an unicode string.
See also durationWin64(). Maximum date is 28 may 60056.
>>> timestampWin64(0)
datetime.datetime(1601, 1, 1, 0, 0)
>>> timestampWin64(127840491566710000)
datetime.datetime(2006, 2, 10, 12, 45, 56, 671000)
"""
try:
return WIN64_TIMESTAMP_T0 + durationWin64(value)
except OverflowError:
raise ValueError(_("date newer than year %s (value=%s)") % (MAXYEAR, value))
# Start of 60-bit UUID timestamp: 15 October 1582 at 00:00
UUID60_TIMESTAMP_T0 = datetime(1582, 10, 15, 0, 0, 0)
def timestampUUID60(value):
"""
Convert UUID 60-bit timestamp to string. The timestamp format is
a 60-bit number which represents number of 100ns since the
the 15 October 1582 at 00:00. Result is an unicode string.
>>> timestampUUID60(0)
datetime.datetime(1582, 10, 15, 0, 0)
>>> timestampUUID60(130435676263032368)
datetime.datetime(1996, 2, 14, 5, 13, 46, 303236)
"""
if not isinstance(value, (float, int, long)):
raise TypeError("an integer or float is required")
if value < 0:
raise ValueError("value have to be a positive or nul integer")
try:
return UUID60_TIMESTAMP_T0 + timedelta(microseconds=value/10)
except OverflowError:
raise ValueError(_("timestampUUID60() overflow (value=%s)") % value)
def humanDatetime(value, strip_microsecond=True):
"""
Convert a timestamp to Unicode string: use ISO format with space separator.
>>> humanDatetime( datetime(2006, 7, 29, 12, 20, 44) )
u'2006-07-29 12:20:44'
>>> humanDatetime( datetime(2003, 6, 30, 16, 0, 5, 370000) )
u'2003-06-30 16:00:05'
>>> humanDatetime( datetime(2003, 6, 30, 16, 0, 5, 370000), False )
u'2003-06-30 16:00:05.370000'
"""
text = unicode(value.isoformat())
text = text.replace('T', ' ')
if strip_microsecond and "." in text:
text = text.split(".")[0]
return text
NEWLINES_REGEX = re.compile("\n+")
def normalizeNewline(text):
r"""
Replace Windows and Mac newlines with Unix newlines.
Replace multiple consecutive newlines with one newline.
>>> normalizeNewline('a\r\nb')
'a\nb'
>>> normalizeNewline('a\r\rb')
'a\nb'
>>> normalizeNewline('a\n\nb')
'a\nb'
"""
text = text.replace("\r\n", "\n")
text = text.replace("\r", "\n")
return NEWLINES_REGEX.sub("\n", text)
-5
View File
@@ -1,5 +0,0 @@
PACKAGE = "hachoir-core"
VERSION = "1.3.4"
WEBSITE = 'http://bitbucket.org/haypo/hachoir/wiki/hachoir-core'
LICENSE = 'GNU GPL v2'
-15
View File
@@ -1,15 +0,0 @@
from hachoir_metadata.version import VERSION as __version__
from hachoir_metadata.metadata import extractMetadata
# Just import the module,
# each module use registerExtractor() method
import hachoir_metadata.archive
import hachoir_metadata.audio
import hachoir_metadata.file_system
import hachoir_metadata.image
import hachoir_metadata.jpeg
import hachoir_metadata.misc
import hachoir_metadata.program
import hachoir_metadata.riff
import hachoir_metadata.video
-166
View File
@@ -1,166 +0,0 @@
from hachoir_metadata.metadata_item import QUALITY_BEST, QUALITY_FASTEST
from hachoir_metadata.safe import fault_tolerant, getValue
from hachoir_metadata.metadata import (
RootMetadata, Metadata, MultipleMetadata, registerExtractor)
from hachoir_parser.archive import (Bzip2Parser, CabFile, GzipParser,
TarFile, ZipFile, MarFile)
from hachoir_core.tools import humanUnixAttributes
from hachoir_core.i18n import _
def maxNbFile(meta):
if meta.quality <= QUALITY_FASTEST:
return 0
if QUALITY_BEST <= meta.quality:
return None
return 1 + int(10 * meta.quality)
def computeCompressionRate(meta):
"""
Compute compression rate, sizes have to be in byte.
"""
if not meta.has("file_size") \
or not meta.get("compr_size", 0):
return
file_size = meta.get("file_size")
if not file_size:
return
meta.compr_rate = float(file_size) / meta.get("compr_size")
class Bzip2Metadata(RootMetadata):
def extract(self, zip):
if "file" in zip:
self.compr_size = zip["file"].size/8
class GzipMetadata(RootMetadata):
def extract(self, gzip):
self.useHeader(gzip)
computeCompressionRate(self)
@fault_tolerant
def useHeader(self, gzip):
self.compression = gzip["compression"].display
if gzip["mtime"]:
self.last_modification = gzip["mtime"].value
self.os = gzip["os"].display
if gzip["has_filename"].value:
self.filename = getValue(gzip, "filename")
if gzip["has_comment"].value:
self.comment = getValue(gzip, "comment")
self.compr_size = gzip["file"].size/8
self.file_size = gzip["size"].value
class ZipMetadata(MultipleMetadata):
def extract(self, zip):
max_nb = maxNbFile(self)
for index, field in enumerate(zip.array("file")):
if max_nb is not None and max_nb <= index:
self.warning("ZIP archive contains many files, but only first %s files are processed" % max_nb)
break
self.processFile(field)
@fault_tolerant
def processFile(self, field):
meta = Metadata(self)
meta.filename = field["filename"].value
meta.creation_date = field["last_mod"].value
meta.compression = field["compression"].display
if "data_desc" in field:
meta.file_size = field["data_desc/file_uncompressed_size"].value
if field["data_desc/file_compressed_size"].value:
meta.compr_size = field["data_desc/file_compressed_size"].value
else:
meta.file_size = field["uncompressed_size"].value
if field["compressed_size"].value:
meta.compr_size = field["compressed_size"].value
computeCompressionRate(meta)
self.addGroup(field.name, meta, "File \"%s\"" % meta.get('filename'))
class TarMetadata(MultipleMetadata):
def extract(self, tar):
max_nb = maxNbFile(self)
for index, field in enumerate(tar.array("file")):
if max_nb is not None and max_nb <= index:
self.warning("TAR archive contains many files, but only first %s files are processed" % max_nb)
break
meta = Metadata(self)
self.extractFile(field, meta)
if meta.has("filename"):
title = _('File "%s"') % meta.getText('filename')
else:
title = _("File")
self.addGroup(field.name, meta, title)
@fault_tolerant
def extractFile(self, field, meta):
meta.filename = field["name"].value
meta.file_attr = humanUnixAttributes(field.getOctal("mode"))
meta.file_size = field.getOctal("size")
try:
if field.getOctal("mtime"):
meta.last_modification = field.getDatetime()
except ValueError:
pass
meta.file_type = field["type"].display
meta.author = "%s (uid=%s), group %s (gid=%s)" %\
(field["uname"].value, field.getOctal("uid"),
field["gname"].value, field.getOctal("gid"))
class CabMetadata(MultipleMetadata):
def extract(self, cab):
if "folder[0]" in cab:
self.useFolder(cab["folder[0]"])
self.format_version = "Microsoft Cabinet version %s" % cab["cab_version"].display
self.comment = "%s folders, %s files" % (
cab["nb_folder"].value, cab["nb_files"].value)
max_nb = maxNbFile(self)
for index, field in enumerate(cab.array("file")):
if max_nb is not None and max_nb <= index:
self.warning("CAB archive contains many files, but only first %s files are processed" % max_nb)
break
self.useFile(field)
@fault_tolerant
def useFolder(self, folder):
compr = folder["compr_method"].display
if folder["compr_method"].value != 0:
compr += " (level %u)" % folder["compr_level"].value
self.compression = compr
@fault_tolerant
def useFile(self, field):
meta = Metadata(self)
meta.filename = field["filename"].value
meta.file_size = field["filesize"].value
meta.creation_date = field["timestamp"].value
attr = field["attributes"].value
if attr != "(none)":
meta.file_attr = attr
if meta.has("filename"):
title = _("File \"%s\"") % meta.getText('filename')
else:
title = _("File")
self.addGroup(field.name, meta, title)
class MarMetadata(MultipleMetadata):
def extract(self, mar):
self.comment = "Contains %s files" % mar["nb_file"].value
self.format_version = "Microsoft Archive version %s" % mar["version"].value
max_nb = maxNbFile(self)
for index, field in enumerate(mar.array("file")):
if max_nb is not None and max_nb <= index:
self.warning("MAR archive contains many files, but only first %s files are processed" % max_nb)
break
meta = Metadata(self)
meta.filename = field["filename"].value
meta.compression = "None"
meta.file_size = field["filesize"].value
self.addGroup(field.name, meta, "File \"%s\"" % meta.getText('filename'))
registerExtractor(CabFile, CabMetadata)
registerExtractor(GzipParser, GzipMetadata)
registerExtractor(Bzip2Parser, Bzip2Metadata)
registerExtractor(TarFile, TarMetadata)
registerExtractor(ZipFile, ZipMetadata)
registerExtractor(MarFile, MarMetadata)
-406
View File
@@ -1,406 +0,0 @@
from hachoir_metadata.metadata import (registerExtractor,
Metadata, RootMetadata, MultipleMetadata)
from hachoir_parser.audio import AuFile, MpegAudioFile, RealAudioFile, AiffFile, FlacParser
from hachoir_parser.container import OggFile, RealMediaFile
from hachoir_core.i18n import _
from hachoir_core.tools import makePrintable, timedelta2seconds, humanBitRate
from datetime import timedelta
from hachoir_metadata.metadata_item import QUALITY_FAST, QUALITY_NORMAL, QUALITY_BEST
from hachoir_metadata.safe import fault_tolerant, getValue
def computeComprRate(meta, size):
if not meta.has("duration") \
or not meta.has("sample_rate") \
or not meta.has("bits_per_sample") \
or not meta.has("nb_channel") \
or not size:
return
orig_size = timedelta2seconds(meta.get("duration")) * meta.get('sample_rate') * meta.get('bits_per_sample') * meta.get('nb_channel')
meta.compr_rate = float(orig_size) / size
def computeBitRate(meta):
if not meta.has("bits_per_sample") \
or not meta.has("nb_channel") \
or not meta.has("sample_rate"):
return
meta.bit_rate = meta.get('bits_per_sample') * meta.get('nb_channel') * meta.get('sample_rate')
VORBIS_KEY_TO_ATTR = {
"ARTIST": "artist",
"ALBUM": "album",
"TRACKNUMBER": "track_number",
"TRACKTOTAL": "track_total",
"ENCODER": "producer",
"TITLE": "title",
"LOCATION": "location",
"DATE": "creation_date",
"ORGANIZATION": "organization",
"GENRE": "music_genre",
"": "comment",
"COMPOSER": "music_composer",
"DESCRIPTION": "comment",
"COMMENT": "comment",
"WWW": "url",
"WOAF": "url",
"LICENSE": "copyright",
}
@fault_tolerant
def readVorbisComment(metadata, comment):
metadata.producer = getValue(comment, "vendor")
for item in comment.array("metadata"):
if "=" in item.value:
key, value = item.value.split("=", 1)
key = key.upper()
if key in VORBIS_KEY_TO_ATTR:
key = VORBIS_KEY_TO_ATTR[key]
setattr(metadata, key, value)
elif value:
metadata.warning("Skip Vorbis comment %s: %s" % (key, value))
class OggMetadata(MultipleMetadata):
def extract(self, ogg):
granule_quotient = None
for index, page in enumerate(ogg.array("page")):
if "segments" not in page:
continue
page = page["segments"]
if "vorbis_hdr" in page:
meta = Metadata(self)
self.vorbisHeader(page["vorbis_hdr"], meta)
self.addGroup("audio[]", meta, "Audio")
if not granule_quotient and meta.has("sample_rate"):
granule_quotient = meta.get('sample_rate')
if "theora_hdr" in page:
meta = Metadata(self)
self.theoraHeader(page["theora_hdr"], meta)
self.addGroup("video[]", meta, "Video")
if "video_hdr" in page:
meta = Metadata(self)
self.videoHeader(page["video_hdr"], meta)
self.addGroup("video[]", meta, "Video")
if not granule_quotient and meta.has("frame_rate"):
granule_quotient = meta.get('frame_rate')
if "comment" in page:
readVorbisComment(self, page["comment"])
if 3 <= index:
# Only process pages 0..3
break
# Compute duration
if granule_quotient and QUALITY_NORMAL <= self.quality:
page = ogg.createLastPage()
if page and "abs_granule_pos" in page:
try:
self.duration = timedelta(seconds=float(page["abs_granule_pos"].value) / granule_quotient)
except OverflowError:
pass
def videoHeader(self, header, meta):
meta.compression = header["fourcc"].display
meta.width = header["width"].value
meta.height = header["height"].value
meta.bits_per_pixel = header["bits_per_sample"].value
if header["time_unit"].value:
meta.frame_rate = 10000000.0 / header["time_unit"].value
def theoraHeader(self, header, meta):
meta.compression = "Theora"
meta.format_version = "Theora version %u.%u (revision %u)" % (\
header["version_major"].value,
header["version_minor"].value,
header["version_revision"].value)
meta.width = header["frame_width"].value
meta.height = header["frame_height"].value
if header["fps_den"].value:
meta.frame_rate = float(header["fps_num"].value) / header["fps_den"].value
if header["aspect_ratio_den"].value:
meta.aspect_ratio = float(header["aspect_ratio_num"].value) / header["aspect_ratio_den"].value
meta.pixel_format = header["pixel_format"].display
meta.comment = "Quality: %s" % header["quality"].value
def vorbisHeader(self, header, meta):
meta.compression = u"Vorbis"
meta.sample_rate = header["audio_sample_rate"].value
meta.nb_channel = header["audio_channels"].value
meta.format_version = u"Vorbis version %s" % header["vorbis_version"].value
meta.bit_rate = header["bitrate_nominal"].value
class AuMetadata(RootMetadata):
def extract(self, audio):
self.sample_rate = audio["sample_rate"].value
self.nb_channel = audio["channels"].value
self.compression = audio["codec"].display
if "info" in audio:
self.comment = audio["info"].value
self.bits_per_sample = audio.getBitsPerSample()
computeBitRate(self)
if "audio_data" in audio:
if self.has("bit_rate"):
self.duration = timedelta(seconds=float(audio["audio_data"].size) / self.get('bit_rate'))
computeComprRate(self, audio["audio_data"].size)
class RealAudioMetadata(RootMetadata):
FOURCC_TO_BITRATE = {
u"28_8": 15200, # 28.8 kbit/sec (audio bit rate: 15.2 kbit/s)
u"14_4": 8000, # 14.4 kbit/sec
u"lpcJ": 8000, # 14.4 kbit/sec
}
def extract(self, real):
version = real["version"].value
if "metadata" in real:
self.useMetadata(real["metadata"])
self.useRoot(real)
self.format_version = "Real audio version %s" % version
if version == 3:
size = getValue(real, "data_size")
elif "filesize" in real and "headersize" in real:
size = (real["filesize"].value + 40) - (real["headersize"].value + 16)
else:
size = None
if size:
size *= 8
if self.has("bit_rate"):
sec = float(size) / self.get('bit_rate')
self.duration = timedelta(seconds=sec)
computeComprRate(self, size)
@fault_tolerant
def useMetadata(self, info):
self.title = info["title"].value
self.author = info["author"].value
self.copyright = info["copyright"].value
self.comment = info["comment"].value
@fault_tolerant
def useRoot(self, real):
self.bits_per_sample = 16 # FIXME: Is that correct?
if real["version"].value != 3:
self.sample_rate = real["sample_rate"].value
self.nb_channel = real["channels"].value
else:
self.sample_rate = 8000
self.nb_channel = 1
fourcc = getValue(real, "FourCC")
if fourcc:
self.compression = fourcc
try:
self.bit_rate = self.FOURCC_TO_BITRATE[fourcc]
except LookupError:
pass
class RealMediaMetadata(MultipleMetadata):
KEY_TO_ATTR = {
"generated by": "producer",
"creation date": "creation_date",
"modification date": "last_modification",
"description": "comment",
}
def extract(self, media):
if "file_prop" in media:
self.useFileProp(media["file_prop"])
if "content_desc" in media:
self.useContentDesc(media["content_desc"])
for index, stream in enumerate(media.array("stream_prop")):
self.useStreamProp(stream, index)
@fault_tolerant
def useFileInfoProp(self, prop):
key = prop["name"].value.lower()
value = prop["value"].value
if key in self.KEY_TO_ATTR:
setattr(self, self.KEY_TO_ATTR[key], value)
elif value:
self.warning("Skip %s: %s" % (prop["name"].value, value))
@fault_tolerant
def useFileProp(self, prop):
self.bit_rate = prop["avg_bit_rate"].value
self.duration = timedelta(milliseconds=prop["duration"].value)
@fault_tolerant
def useContentDesc(self, content):
self.title = content["title"].value
self.author = content["author"].value
self.copyright = content["copyright"].value
self.comment = content["comment"].value
@fault_tolerant
def useStreamProp(self, stream, index):
meta = Metadata(self)
meta.comment = "Start: %s" % stream["stream_start"].value
if getValue(stream, "mime_type") == "logical-fileinfo":
for prop in stream.array("file_info/prop"):
self.useFileInfoProp(prop)
else:
meta.bit_rate = stream["avg_bit_rate"].value
meta.duration = timedelta(milliseconds=stream["duration"].value)
meta.mime_type = getValue(stream, "mime_type")
meta.title = getValue(stream, "desc")
self.addGroup("stream[%u]" % index, meta, "Stream #%u" % (1+index))
class MpegAudioMetadata(RootMetadata):
TAG_TO_KEY = {
# ID3 version 2.2
"TP1": "author",
"COM": "comment",
"TEN": "producer",
"TRK": "track_number",
"TAL": "album",
"TT2": "title",
"TYE": "creation_date",
"TCO": "music_genre",
# ID3 version 2.3+
"TPE1": "author",
"COMM": "comment",
"TENC": "producer",
"TRCK": "track_number",
"TALB": "album",
"TIT2": "title",
"TYER": "creation_date",
"WXXX": "url",
"TCON": "music_genre",
"TLAN": "language",
"TCOP": "copyright",
"TDAT": "creation_date",
"TRDA": "creation_date",
"TORY": "creation_date",
"TIT1": "title",
}
def processID3v2(self, field):
# Read value
if "content" not in field:
return
content = field["content"]
if "text" not in content:
return
if "title" in content and content["title"].value:
value = "%s: %s" % (content["title"].value, content["text"].value)
else:
value = content["text"].value
# Known tag?
tag = field["tag"].value
if tag not in self.TAG_TO_KEY:
if tag:
if isinstance(tag, str):
tag = makePrintable(tag, "ISO-8859-1", to_unicode=True)
self.warning("Skip ID3v2 tag %s: %s" % (tag, value))
return
key = self.TAG_TO_KEY[tag]
setattr(self, key, value)
def readID3v2(self, id3):
for field in id3:
if field.is_field_set and "tag" in field:
self.processID3v2(field)
def extract(self, mp3):
if "/frames/frame[0]" in mp3:
frame = mp3["/frames/frame[0]"]
self.nb_channel = (frame.getNbChannel(), frame["channel_mode"].display)
self.format_version = u"MPEG version %s layer %s" % \
(frame["version"].display, frame["layer"].display)
self.sample_rate = frame.getSampleRate()
self.bits_per_sample = 16
if mp3["frames"].looksConstantBitRate():
self.computeBitrate(frame)
else:
self.computeVariableBitrate(mp3)
if "id3v1" in mp3:
id3 = mp3["id3v1"]
self.comment = id3["comment"].value
self.author = id3["author"].value
self.title = id3["song"].value
self.album = id3["album"].value
if id3["year"].value != "0":
self.creation_date = id3["year"].value
if "track_nb" in id3:
self.track_number = id3["track_nb"].value
if "id3v2" in mp3:
self.readID3v2(mp3["id3v2"])
if "frames" in mp3:
computeComprRate(self, mp3["frames"].size)
def computeBitrate(self, frame):
bit_rate = frame.getBitRate() # may returns None on error
if not bit_rate:
return
self.bit_rate = (bit_rate, _("%s (constant)") % humanBitRate(bit_rate))
self.duration = timedelta(seconds=float(frame["/frames"].size) / bit_rate)
def computeVariableBitrate(self, mp3):
if self.quality <= QUALITY_FAST:
return
count = 0
if QUALITY_BEST <= self.quality:
self.warning("Process all MPEG audio frames to compute exact duration")
max_count = None
else:
max_count = 500 * self.quality
total_bit_rate = 0.0
for index, frame in enumerate(mp3.array("frames/frame")):
if index < 3:
continue
bit_rate = frame.getBitRate()
if bit_rate:
total_bit_rate += float(bit_rate)
count += 1
if max_count and max_count <= count:
break
if not count:
return
bit_rate = total_bit_rate / count
self.bit_rate = (bit_rate,
_("%s (Variable bit rate)") % humanBitRate(bit_rate))
duration = timedelta(seconds=float(mp3["frames"].size) / bit_rate)
self.duration = duration
class AiffMetadata(RootMetadata):
def extract(self, aiff):
if "common" in aiff:
self.useCommon(aiff["common"])
computeBitRate(self)
@fault_tolerant
def useCommon(self, info):
self.nb_channel = info["nb_channel"].value
self.bits_per_sample = info["sample_size"].value
self.sample_rate = getValue(info, "sample_rate")
if self.has("sample_rate"):
rate = self.get("sample_rate")
if rate:
sec = float(info["nb_sample"].value) / rate
self.duration = timedelta(seconds=sec)
if "codec" in info:
self.compression = info["codec"].display
class FlacMetadata(RootMetadata):
def extract(self, flac):
if "metadata/stream_info/content" in flac:
self.useStreamInfo(flac["metadata/stream_info/content"])
if "metadata/comment/content" in flac:
readVorbisComment(self, flac["metadata/comment/content"])
@fault_tolerant
def useStreamInfo(self, info):
self.nb_channel = info["nb_channel"].value + 1
self.bits_per_sample = info["bits_per_sample"].value + 1
self.sample_rate = info["sample_hertz"].value
sec = info["total_samples"].value
if sec:
sec = float(sec) / info["sample_hertz"].value
self.duration = timedelta(seconds=sec)
registerExtractor(AuFile, AuMetadata)
registerExtractor(MpegAudioFile, MpegAudioMetadata)
registerExtractor(OggFile, OggMetadata)
registerExtractor(RealMediaFile, RealMediaMetadata)
registerExtractor(RealAudioFile, RealAudioMetadata)
registerExtractor(AiffFile, AiffMetadata)
registerExtractor(FlacParser, FlacMetadata)
-2
View File
@@ -1,2 +0,0 @@
MAX_STR_LENGTH = 300 # characters
RAW_OUTPUT = False
-28
View File
@@ -1,28 +0,0 @@
from hachoir_metadata.metadata import RootMetadata, registerExtractor
from hachoir_metadata.safe import fault_tolerant
from hachoir_parser.file_system import ISO9660
from datetime import datetime
class ISO9660_Metadata(RootMetadata):
def extract(self, iso):
desc = iso['volume[0]/content']
self.title = desc['volume_id'].value
self.title = desc['vol_set_id'].value
self.author = desc['publisher'].value
self.author = desc['data_preparer'].value
self.producer = desc['application'].value
self.copyright = desc['copyright'].value
self.readTimestamp('creation_date', desc['creation_ts'].value)
self.readTimestamp('last_modification', desc['modification_ts'].value)
@fault_tolerant
def readTimestamp(self, key, value):
if value.startswith("0000"):
return
value = datetime(
int(value[0:4]), int(value[4:6]), int(value[6:8]),
int(value[8:10]), int(value[10:12]), int(value[12:14]))
setattr(self, key, value)
registerExtractor(ISO9660, ISO9660_Metadata)
-52
View File
@@ -1,52 +0,0 @@
from hachoir_metadata.timezone import UTC
from datetime import date, datetime
# Year in 1850..2030
MIN_YEAR = 1850
MAX_YEAR = 2030
class Filter:
def __init__(self, valid_types, min=None, max=None):
self.types = valid_types
self.min = min
self.max = max
def __call__(self, value):
if not isinstance(value, self.types):
return True
if self.min is not None and value < self.min:
return False
if self.max is not None and self.max < value:
return False
return True
class NumberFilter(Filter):
def __init__(self, min=None, max=None):
Filter.__init__(self, (int, long, float), min, max)
class DatetimeFilter(Filter):
def __init__(self, min=None, max=None):
Filter.__init__(self, (date, datetime),
datetime(MIN_YEAR, 1, 1),
datetime(MAX_YEAR, 12, 31))
self.min_date = date(MIN_YEAR, 1, 1)
self.max_date = date(MAX_YEAR, 12, 31)
self.min_tz = datetime(MIN_YEAR, 1, 1, tzinfo=UTC)
self.max_tz = datetime(MAX_YEAR, 12, 31, tzinfo=UTC)
def __call__(self, value):
"""
Use different min/max values depending on value type
(datetime with timezone, datetime or date).
"""
if not isinstance(value, self.types):
return True
if hasattr(value, "tzinfo") and value.tzinfo:
return (self.min_tz <= value <= self.max_tz)
elif isinstance(value, datetime):
return (self.min <= value <= self.max)
else:
return (self.min_date <= value <= self.max_date)
DATETIME_FILTER = DatetimeFilter()
-25
View File
@@ -1,25 +0,0 @@
from hachoir_core.i18n import _, ngettext
NB_CHANNEL_NAME = {1: _("mono"), 2: _("stereo")}
def humanAudioChannel(value):
return NB_CHANNEL_NAME.get(value, unicode(value))
def humanFrameRate(value):
if isinstance(value, (int, long, float)):
return _("%.1f fps") % value
else:
return value
def humanComprRate(rate):
return u"%.1fx" % rate
def humanAltitude(value):
return ngettext("%.1f meter", "%.1f meters", value) % value
def humanPixelSize(value):
return ngettext("%s pixel", "%s pixels", value) % value
def humanDPI(value):
return u"%s DPI" % value
-299
View File
@@ -1,299 +0,0 @@
from hachoir_metadata.metadata import (registerExtractor,
Metadata, RootMetadata, MultipleMetadata)
from hachoir_parser.image import (
BmpFile, IcoFile, PcxFile, GifFile, PngFile, TiffFile,
XcfFile, TargaFile, WMF_File, PsdFile)
from hachoir_parser.image.png import getBitsPerPixel as pngBitsPerPixel
from hachoir_parser.image.xcf import XcfProperty
from hachoir_core.i18n import _
from hachoir_metadata.safe import fault_tolerant
def computeComprRate(meta, compr_size):
"""
Compute image compression rate. Skip size of color palette, focus on
image pixels. Original size is width x height x bpp. Compressed size
is an argument (in bits).
Set "compr_data" with a string like "1.52x".
"""
if not meta.has("width") \
or not meta.has("height") \
or not meta.has("bits_per_pixel"):
return
if not compr_size:
return
orig_size = meta.get('width') * meta.get('height') * meta.get('bits_per_pixel')
meta.compr_rate = float(orig_size) / compr_size
class BmpMetadata(RootMetadata):
def extract(self, image):
if "header" not in image:
return
hdr = image["header"]
self.width = hdr["width"].value
self.height = hdr["height"].value
bpp = hdr["bpp"].value
if bpp:
if bpp <= 8 and "used_colors" in hdr:
self.nb_colors = hdr["used_colors"].value
self.bits_per_pixel = bpp
self.compression = hdr["compression"].display
self.format_version = u"Microsoft Bitmap version %s" % hdr.getFormatVersion()
self.width_dpi = hdr["horizontal_dpi"].value
self.height_dpi = hdr["vertical_dpi"].value
if "pixels" in image:
computeComprRate(self, image["pixels"].size)
class TiffMetadata(RootMetadata):
key_to_attr = {
"img_width": "width",
"img_height": "width",
# TODO: Enable that (need link to value)
# "description": "comment",
# "doc_name": "title",
# "orientation": "image_orientation",
}
def extract(self, tiff):
if "ifd" in tiff:
self.useIFD(tiff["ifd"])
def useIFD(self, ifd):
for field in ifd:
try:
attrname = self.key_to_attr[field.name]
except KeyError:
continue
if "value" not in field:
continue
value = field["value"].value
setattr(self, attrname, value)
class IcoMetadata(MultipleMetadata):
color_to_bpp = {
2: 1,
16: 4,
256: 8
}
def extract(self, icon):
for index, header in enumerate(icon.array("icon_header")):
image = Metadata(self)
# Read size and colors from header
image.width = header["width"].value
image.height = header["height"].value
bpp = header["bpp"].value
nb_colors = header["nb_color"].value
if nb_colors != 0:
image.nb_colors = nb_colors
if bpp == 0 and nb_colors in self.color_to_bpp:
bpp = self.color_to_bpp[nb_colors]
elif bpp == 0:
bpp = 8
image.bits_per_pixel = bpp
image.setHeader(_("Icon #%u (%sx%s)")
% (1+index, image.get("width", "?"), image.get("height", "?")))
# Read compression from data (if available)
key = "icon_data[%u]/header/codec" % index
if key in icon:
image.compression = icon[key].display
key = "icon_data[%u]/pixels" % index
if key in icon:
computeComprRate(image, icon[key].size)
# Store new image
self.addGroup("image[%u]" % index, image)
class PcxMetadata(RootMetadata):
@fault_tolerant
def extract(self, pcx):
self.width = 1 + pcx["xmax"].value
self.height = 1 + pcx["ymax"].value
self.width_dpi = pcx["horiz_dpi"].value
self.height_dpi = pcx["vert_dpi"].value
self.bits_per_pixel = pcx["bpp"].value
if 1 <= pcx["bpp"].value <= 8:
self.nb_colors = 2 ** pcx["bpp"].value
self.compression = _("Run-length encoding (RLE)")
self.format_version = "PCX: %s" % pcx["version"].display
if "image_data" in pcx:
computeComprRate(self, pcx["image_data"].size)
class XcfMetadata(RootMetadata):
# Map image type to bits/pixel
TYPE_TO_BPP = {0: 24, 1: 8, 2: 8}
def extract(self, xcf):
self.width = xcf["width"].value
self.height = xcf["height"].value
try:
self.bits_per_pixel = self.TYPE_TO_BPP[ xcf["type"].value ]
except KeyError:
pass
self.format_version = xcf["type"].display
self.readProperties(xcf)
@fault_tolerant
def processProperty(self, prop):
type = prop["type"].value
if type == XcfProperty.PROP_PARASITES:
for field in prop["data"]:
if "name" not in field or "data" not in field:
continue
if field["name"].value == "gimp-comment":
self.comment = field["data"].value
elif type == XcfProperty.PROP_COMPRESSION:
self.compression = prop["data/compression"].display
elif type == XcfProperty.PROP_RESOLUTION:
self.width_dpi = int(prop["data/xres"].value)
self.height_dpi = int(prop["data/yres"].value)
def readProperties(self, xcf):
for prop in xcf.array("property"):
self.processProperty(prop)
class PngMetadata(RootMetadata):
TEXT_TO_ATTR = {
"software": "producer",
}
def extract(self, png):
if "header" in png:
self.useHeader(png["header"])
if "time" in png:
self.useTime(png["time"])
if "physical" in png:
self.usePhysical(png["physical"])
for comment in png.array("text"):
if "text" not in comment:
continue
keyword = comment["keyword"].value
text = comment["text"].value
try:
key = self.TEXT_TO_ATTR[keyword.lower()]
setattr(self, key, text)
except KeyError:
if keyword.lower() != "comment":
self.comment = "%s=%s" % (keyword, text)
else:
self.comment = text
compr_size = sum( data.size for data in png.array("data") )
computeComprRate(self, compr_size)
@fault_tolerant
def useTime(self, field):
self.creation_date = field.value
@fault_tolerant
def usePhysical(self, field):
self.width_dpi = field["pixel_per_unit_x"].value
self.height_dpi = field["pixel_per_unit_y"].value
@fault_tolerant
def useHeader(self, header):
self.width = header["width"].value
self.height = header["height"].value
# Read number of colors and pixel format
if "/palette/size" in header:
nb_colors = header["/palette/size"].value // 3
else:
nb_colors = None
if not header["has_palette"].value:
if header["has_alpha"].value:
self.pixel_format = _("RGBA")
else:
self.pixel_format = _("RGB")
elif "/transparency" in header:
self.pixel_format = _("Color index with transparency")
if nb_colors:
nb_colors -= 1
else:
self.pixel_format = _("Color index")
self.bits_per_pixel = pngBitsPerPixel(header)
if nb_colors:
self.nb_colors = nb_colors
# Read compression, timestamp, etc.
self.compression = header["compression"].display
class GifMetadata(RootMetadata):
def extract(self, gif):
self.useScreen(gif["/screen"])
if self.has("bits_per_pixel"):
self.nb_colors = (1 << self.get('bits_per_pixel'))
self.compression = _("LZW")
self.format_version = "GIF version %s" % gif["version"].value
for comments in gif.array("comments"):
for comment in gif.array(comments.name + "/comment"):
self.comment = comment.value
if "graphic_ctl/has_transp" in gif and gif["graphic_ctl/has_transp"].value:
self.pixel_format = _("Color index with transparency")
else:
self.pixel_format = _("Color index")
@fault_tolerant
def useScreen(self, screen):
self.width = screen["width"].value
self.height = screen["height"].value
self.bits_per_pixel = (1 + screen["bpp"].value)
class TargaMetadata(RootMetadata):
def extract(self, tga):
self.width = tga["width"].value
self.height = tga["height"].value
self.bits_per_pixel = tga["bpp"].value
if tga["nb_color"].value:
self.nb_colors = tga["nb_color"].value
self.compression = tga["codec"].display
if "pixels" in tga:
computeComprRate(self, tga["pixels"].size)
class WmfMetadata(RootMetadata):
def extract(self, wmf):
if wmf.isAPM():
if "amf_header/rect" in wmf:
rect = wmf["amf_header/rect"]
self.width = (rect["right"].value - rect["left"].value)
self.height = (rect["bottom"].value - rect["top"].value)
self.bits_per_pixel = 24
elif wmf.isEMF():
emf = wmf["emf_header"]
if "description" in emf:
desc = emf["description"].value
if "\0" in desc:
self.producer, self.title = desc.split("\0", 1)
else:
self.producer = desc
if emf["nb_colors"].value:
self.nb_colors = emf["nb_colors"].value
self.bits_per_pixel = 8
else:
self.bits_per_pixel = 24
self.width = emf["width_px"].value
self.height = emf["height_px"].value
class PsdMetadata(RootMetadata):
@fault_tolerant
def extract(self, psd):
self.width = psd["width"].value
self.height = psd["height"].value
self.bits_per_pixel = psd["depth"].value * psd["nb_channels"].value
self.pixel_format = psd["color_mode"].display
self.compression = psd["compression"].display
registerExtractor(IcoFile, IcoMetadata)
registerExtractor(GifFile, GifMetadata)
registerExtractor(XcfFile, XcfMetadata)
registerExtractor(TargaFile, TargaMetadata)
registerExtractor(PcxFile, PcxMetadata)
registerExtractor(BmpFile, BmpMetadata)
registerExtractor(PngFile, PngMetadata)
registerExtractor(TiffFile, TiffMetadata)
registerExtractor(WMF_File, WmfMetadata)
registerExtractor(PsdFile, PsdMetadata)
-289
View File
@@ -1,289 +0,0 @@
from hachoir_metadata.metadata import RootMetadata, registerExtractor
from hachoir_metadata.image import computeComprRate
from hachoir_parser.image.exif import ExifEntry
from hachoir_parser.image.jpeg import (
JpegFile, JpegChunk,
QUALITY_HASH_COLOR, QUALITY_SUM_COLOR,
QUALITY_HASH_GRAY, QUALITY_SUM_GRAY)
from hachoir_core.field import MissingField
from hachoir_core.i18n import _
from hachoir_core.tools import makeUnicode
from hachoir_metadata.safe import fault_tolerant
from datetime import datetime
def deg2float(degree, minute, second):
return degree + (float(minute) + float(second) / 60.0) / 60.0
class JpegMetadata(RootMetadata):
EXIF_KEY = {
# Exif metadatas
ExifEntry.TAG_CAMERA_MANUFACTURER: "camera_manufacturer",
ExifEntry.TAG_CAMERA_MODEL: "camera_model",
ExifEntry.TAG_ORIENTATION: "image_orientation",
ExifEntry.TAG_EXPOSURE: "camera_exposure",
ExifEntry.TAG_FOCAL: "camera_focal",
ExifEntry.TAG_BRIGHTNESS: "camera_brightness",
ExifEntry.TAG_APERTURE: "camera_aperture",
# Generic metadatas
ExifEntry.TAG_IMG_TITLE: "title",
ExifEntry.TAG_SOFTWARE: "producer",
ExifEntry.TAG_FILE_TIMESTAMP: "creation_date",
ExifEntry.TAG_WIDTH: "width",
ExifEntry.TAG_HEIGHT: "height",
ExifEntry.TAG_USER_COMMENT: "comment",
}
IPTC_KEY = {
80: "author",
90: "city",
101: "country",
116: "copyright",
120: "title",
231: "comment",
}
orientation_name = {
1: _('Horizontal (normal)'),
2: _('Mirrored horizontal'),
3: _('Rotated 180'),
4: _('Mirrored vertical'),
5: _('Mirrored horizontal then rotated 90 counter-clock-wise'),
6: _('Rotated 90 clock-wise'),
7: _('Mirrored horizontal then rotated 90 clock-wise'),
8: _('Rotated 90 counter clock-wise'),
}
def extract(self, jpeg):
if "start_frame/content" in jpeg:
self.startOfFrame(jpeg["start_frame/content"])
elif "start_scan/content/nr_components" in jpeg:
self.bits_per_pixel = 8 * jpeg["start_scan/content/nr_components"].value
if "app0/content" in jpeg:
self.extractAPP0(jpeg["app0/content"])
if "exif/content" in jpeg:
for ifd in jpeg.array("exif/content/ifd"):
for entry in ifd.array("entry"):
self.processIfdEntry(ifd, entry)
self.readGPS(ifd)
if "photoshop/content" in jpeg:
psd = jpeg["photoshop/content"]
if "version/content/reader_name" in psd:
self.producer = psd["version/content/reader_name"].value
if "iptc/content" in psd:
self.parseIPTC(psd["iptc/content"])
for field in jpeg.array("comment"):
if "content/comment" in field:
self.comment = field["content/comment"].value
self.computeQuality(jpeg)
if "data" in jpeg:
computeComprRate(self, jpeg["data"].size)
if not self.has("producer") and "photoshop" in jpeg:
self.producer = u"Adobe Photoshop"
if self.has("compression"):
self.compression = "JPEG"
@fault_tolerant
def startOfFrame(self, sof):
# Set compression method
key = sof["../type"].value
self.compression = "JPEG (%s)" % JpegChunk.START_OF_FRAME[key]
# Read image size and bits/pixel
self.width = sof["width"].value
self.height = sof["height"].value
nb_components = sof["nr_components"].value
self.bits_per_pixel = 8 * nb_components
if nb_components == 3:
self.pixel_format = _("YCbCr")
elif nb_components == 1:
self.pixel_format = _("Grayscale")
self.nb_colors = 256
@fault_tolerant
def computeQuality(self, jpeg):
# This function is an adaption to Python of ImageMagick code
# to compute JPEG quality using quantization tables
# Read quantization tables
qtlist = []
for dqt in jpeg.array("quantization"):
for qt in dqt.array("content/qt"):
# TODO: Take care of qt["index"].value?
qtlist.append(qt)
if not qtlist:
return
# Compute sum of all coefficients
sumcoeff = 0
for qt in qtlist:
coeff = qt.array("coeff")
for index in xrange(64):
sumcoeff += coeff[index].value
# Choose the right quality table and compute hash value
try:
hashval= qtlist[0]["coeff[2]"].value + qtlist[0]["coeff[53]"].value
if 2 <= len(qtlist):
hashval += qtlist[1]["coeff[0]"].value + qtlist[1]["coeff[63]"].value
hashtable = QUALITY_HASH_COLOR
sumtable = QUALITY_SUM_COLOR
else:
hashtable = QUALITY_HASH_GRAY
sumtable = QUALITY_SUM_GRAY
except (MissingField, IndexError):
# A coefficient is missing, so don't compute JPEG quality
return
# Find the JPEG quality
for index in xrange(100):
if (hashval >= hashtable[index]) or (sumcoeff >= sumtable[index]):
quality = "%s%%" % (index + 1)
if (hashval > hashtable[index]) or (sumcoeff > sumtable[index]):
quality += " " + _("(approximate)")
self.comment = "JPEG quality: %s" % quality
return
@fault_tolerant
def extractAPP0(self, app0):
self.format_version = u"JFIF %u.%02u" \
% (app0["ver_maj"].value, app0["ver_min"].value)
if "y_density" in app0:
self.width_dpi = app0["x_density"].value
self.height_dpi = app0["y_density"].value
@fault_tolerant
def processIfdEntry(self, ifd, entry):
# Skip unknown tags
tag = entry["tag"].value
if tag not in self.EXIF_KEY:
return
key = self.EXIF_KEY[tag]
if key in ("width", "height") and self.has(key):
# EXIF "valid size" are sometimes not updated when the image is scaled
# so we just ignore it
return
# Read value
if "value" in entry:
value = entry["value"].value
else:
value = ifd["value_%s" % entry.name].value
# Convert value to string
if tag == ExifEntry.TAG_ORIENTATION:
value = self.orientation_name.get(value, value)
elif tag == ExifEntry.TAG_EXPOSURE:
if not value:
return
if isinstance(value, float):
value = (value, u"1/%g" % (1/value))
elif entry["type"].value in (ExifEntry.TYPE_RATIONAL, ExifEntry.TYPE_SIGNED_RATIONAL):
value = (value, u"%.3g" % value)
# Store information
setattr(self, key, value)
@fault_tolerant
def readGPS(self, ifd):
# Read latitude and longitude
latitude_ref = None
longitude_ref = None
latitude = None
longitude = None
altitude_ref = 1
altitude = None
timestamp = None
datestamp = None
for entry in ifd.array("entry"):
tag = entry["tag"].value
if tag == ExifEntry.TAG_GPS_LATITUDE_REF:
if entry["value"].value == "N":
latitude_ref = 1
else:
latitude_ref = -1
elif tag == ExifEntry.TAG_GPS_LONGITUDE_REF:
if entry["value"].value == "E":
longitude_ref = 1
else:
longitude_ref = -1
elif tag == ExifEntry.TAG_GPS_ALTITUDE_REF:
if entry["value"].value == 1:
altitude_ref = -1
else:
altitude_ref = 1
elif tag == ExifEntry.TAG_GPS_LATITUDE:
latitude = [ifd["value_%s[%u]" % (entry.name, index)].value for index in xrange(3)]
elif tag == ExifEntry.TAG_GPS_LONGITUDE:
longitude = [ifd["value_%s[%u]" % (entry.name, index)].value for index in xrange(3)]
elif tag == ExifEntry.TAG_GPS_ALTITUDE:
altitude = ifd["value_%s" % entry.name].value
elif tag == ExifEntry.TAG_GPS_DATESTAMP:
datestamp = ifd["value_%s" % entry.name].value
elif tag == ExifEntry.TAG_GPS_TIMESTAMP:
items = [ifd["value_%s[%u]" % (entry.name, index)].value for index in xrange(3)]
items = map(int, items)
items = map(str, items)
timestamp = ":".join(items)
if latitude_ref and latitude:
value = deg2float(*latitude)
if latitude_ref < 0:
value = -value
self.latitude = value
if longitude and longitude_ref:
value = deg2float(*longitude)
if longitude_ref < 0:
value = -value
self.longitude = value
if altitude:
value = altitude
if altitude_ref < 0:
value = -value
self.altitude = value
if datestamp:
if timestamp:
datestamp += " " + timestamp
self.creation_date = datestamp
def parseIPTC(self, iptc):
datestr = hourstr = None
for field in iptc:
# Skip incomplete field
if "tag" not in field or "content" not in field:
continue
# Get value
value = field["content"].value
if isinstance(value, (str, unicode)):
value = value.replace("\r", " ")
value = value.replace("\n", " ")
# Skip unknown tag
tag = field["tag"].value
if tag == 55:
datestr = value
continue
if tag == 60:
hourstr = value
continue
if tag not in self.IPTC_KEY:
if tag != 0:
self.warning("Skip IPTC key %s: %s" % (
field["tag"].display, makeUnicode(value)))
continue
setattr(self, self.IPTC_KEY[tag], value)
if datestr and hourstr:
try:
year = int(datestr[0:4])
month = int(datestr[4:6])
day = int(datestr[6:8])
hour = int(hourstr[0:2])
min = int(hourstr[2:4])
sec = int(hourstr[4:6])
self.creation_date = datetime(year, month, day, hour, min, sec)
except ValueError:
pass
registerExtractor(JpegFile, JpegMetadata)
-291
View File
@@ -1,291 +0,0 @@
# -*- coding: utf-8 -*-
from hachoir_core.compatibility import any, sorted
from hachoir_core.endian import endian_name
from hachoir_core.tools import makePrintable, makeUnicode
from hachoir_core.dict import Dict
from hachoir_core.error import error, HACHOIR_ERRORS
from hachoir_core.i18n import _
from hachoir_core.log import Logger
from hachoir_metadata.metadata_item import (
MIN_PRIORITY, MAX_PRIORITY, QUALITY_NORMAL)
from hachoir_metadata.register import registerAllItems
extractors = {}
class Metadata(Logger):
header = u"Metadata"
def __init__(self, parent, quality=QUALITY_NORMAL):
assert isinstance(self.header, unicode)
# Limit to 0.0 .. 1.0
if parent:
quality = parent.quality
else:
quality = min(max(0.0, quality), 1.0)
object.__init__(self)
object.__setattr__(self, "_Metadata__data", {})
object.__setattr__(self, "quality", quality)
header = self.__class__.header
object.__setattr__(self, "_Metadata__header", header)
registerAllItems(self)
def _logger(self):
pass
def __setattr__(self, key, value):
"""
Add a new value to data with name 'key'. Skip duplicates.
"""
# Invalid key?
if key not in self.__data:
raise KeyError(_("%s has no metadata '%s'") % (self.__class__.__name__, key))
# Skip duplicates
self.__data[key].add(value)
def setHeader(self, text):
object.__setattr__(self, "header", text)
def getItems(self, key):
try:
return self.__data[key]
except LookupError:
raise ValueError("Metadata has no value '%s'" % key)
def getItem(self, key, index):
try:
return self.getItems(key)[index]
except (LookupError, ValueError):
return None
def has(self, key):
return 1 <= len(self.getItems(key))
def get(self, key, default=None, index=0):
"""
Read first value of tag with name 'key'.
>>> from datetime import timedelta
>>> a = RootMetadata()
>>> a.duration = timedelta(seconds=2300)
>>> a.get('duration')
datetime.timedelta(0, 2300)
>>> a.get('author', u'Anonymous')
u'Anonymous'
"""
item = self.getItem(key, index)
if item is None:
if default is None:
raise ValueError("Metadata has no value '%s' (index %s)" % (key, index))
else:
return default
return item.value
def getValues(self, key):
try:
data = self.__data[key]
except LookupError:
raise ValueError("Metadata has no value '%s'" % key)
return [ item.value for item in data ]
def getText(self, key, default=None, index=0):
"""
Read first value, as unicode string, of tag with name 'key'.
>>> from datetime import timedelta
>>> a = RootMetadata()
>>> a.duration = timedelta(seconds=2300)
>>> a.getText('duration')
u'38 min 20 sec'
>>> a.getText('titre', u'Unknown')
u'Unknown'
"""
item = self.getItem(key, index)
if item is not None:
return item.text
else:
return default
def register(self, data):
assert data.key not in self.__data
data.metadata = self
self.__data[data.key] = data
def __iter__(self):
return self.__data.itervalues()
def __str__(self):
r"""
Create a multi-line ASCII string (end of line is "\n") which
represents all datas.
>>> a = RootMetadata()
>>> a.author = "haypo"
>>> a.copyright = unicode("© Hachoir", "UTF-8")
>>> print a
Metadata:
- Author: haypo
- Copyright: \xa9 Hachoir
@see __unicode__() and exportPlaintext()
"""
text = self.exportPlaintext()
return "\n".join( makePrintable(line, "ASCII") for line in text )
def __unicode__(self):
r"""
Create a multi-line Unicode string (end of line is "\n") which
represents all datas.
>>> a = RootMetadata()
>>> a.copyright = unicode("© Hachoir", "UTF-8")
>>> print repr(unicode(a))
u'Metadata:\n- Copyright: \xa9 Hachoir'
@see __str__() and exportPlaintext()
"""
return "\n".join(self.exportPlaintext())
def exportPlaintext(self, priority=None, human=True, line_prefix=u"- ", title=None):
r"""
Convert metadata to multi-line Unicode string and skip datas
with priority lower than specified priority.
Default priority is Metadata.MAX_PRIORITY. If human flag is True, data
key are translated to better human name (eg. "bit_rate" becomes
"Bit rate") which may be translated using gettext.
If priority is too small, metadata are empty and so None is returned.
>>> print RootMetadata().exportPlaintext()
None
>>> meta = RootMetadata()
>>> meta.copyright = unicode("© Hachoir", "UTF-8")
>>> print repr(meta.exportPlaintext())
[u'Metadata:', u'- Copyright: \xa9 Hachoir']
@see __str__() and __unicode__()
"""
if priority is not None:
priority = max(priority, MIN_PRIORITY)
priority = min(priority, MAX_PRIORITY)
else:
priority = MAX_PRIORITY
if not title:
title = self.header
text = ["%s:" % title]
for data in sorted(self):
if priority < data.priority:
break
if not data.values:
continue
if human:
title = data.description
else:
title = data.key
for item in data.values:
if human:
value = item.text
else:
value = makeUnicode(item.value)
text.append("%s%s: %s" % (line_prefix, title, value))
if 1 < len(text):
return text
else:
return None
def __nonzero__(self):
return any(item for item in self.__data.itervalues())
class RootMetadata(Metadata):
def __init__(self, quality=QUALITY_NORMAL):
Metadata.__init__(self, None, quality)
class MultipleMetadata(RootMetadata):
header = _("Common")
def __init__(self, quality=QUALITY_NORMAL):
RootMetadata.__init__(self, quality)
object.__setattr__(self, "_MultipleMetadata__groups", Dict())
object.__setattr__(self, "_MultipleMetadata__key_counter", {})
def __contains__(self, key):
return key in self.__groups
def __getitem__(self, key):
return self.__groups[key]
def iterGroups(self):
return self.__groups.itervalues()
def __nonzero__(self):
if RootMetadata.__nonzero__(self):
return True
return any(bool(group) for group in self.__groups)
def addGroup(self, key, metadata, header=None):
"""
Add a new group (metadata of a sub-document).
Returns False if the group is skipped, True if it has been added.
"""
if not metadata:
self.warning("Skip empty group %s" % key)
return False
if key.endswith("[]"):
key = key[:-2]
if key in self.__key_counter:
self.__key_counter[key] += 1
else:
self.__key_counter[key] = 1
key += "[%u]" % self.__key_counter[key]
if header:
metadata.setHeader(header)
self.__groups.append(key, metadata)
return True
def exportPlaintext(self, priority=None, human=True, line_prefix=u"- "):
common = Metadata.exportPlaintext(self, priority, human, line_prefix)
if common:
text = common
else:
text = []
for key, metadata in self.__groups.iteritems():
if not human:
title = key
else:
title = None
value = metadata.exportPlaintext(priority, human, line_prefix, title=title)
if value:
text.extend(value)
if len(text):
return text
else:
return None
def registerExtractor(parser, extractor):
assert parser not in extractors
assert issubclass(extractor, RootMetadata)
extractors[parser] = extractor
def extractMetadata(parser, quality=QUALITY_NORMAL):
"""
Create a Metadata class from a parser. Returns None if no metadata
extractor does exist for the parser class.
"""
try:
extractor = extractors[parser.__class__]
except KeyError:
return None
metadata = extractor(quality)
try:
metadata.extract(parser)
except HACHOIR_ERRORS, err:
error("Error during metadata extraction: %s" % unicode(err))
if metadata:
metadata.mime_type = parser.mime_type
metadata.endian = endian_name[parser.endian]
return metadata
-146
View File
@@ -1,146 +0,0 @@
from hachoir_core.tools import makeUnicode, normalizeNewline
from hachoir_core.error import HACHOIR_ERRORS
from hachoir_metadata import config
from hachoir_metadata.setter import normalizeString
MIN_PRIORITY = 100
MAX_PRIORITY = 999
QUALITY_FASTEST = 0.0
QUALITY_FAST = 0.25
QUALITY_NORMAL = 0.5
QUALITY_GOOD = 0.75
QUALITY_BEST = 1.0
class DataValue:
def __init__(self, value, text):
self.value = value
self.text = text
class Data:
def __init__(self, key, priority, description,
text_handler=None, type=None, filter=None, conversion=None):
"""
handler is only used if value is not string nor unicode, prototype:
def handler(value) -> str/unicode
"""
assert MIN_PRIORITY <= priority <= MAX_PRIORITY
assert isinstance(description, unicode)
self.metadata = None
self.key = key
self.description = description
self.values = []
if type and not isinstance(type, (tuple, list)):
type = (type,)
self.type = type
self.text_handler = text_handler
self.filter = filter
self.priority = priority
self.conversion = conversion
def _createItem(self, value, text=None):
if text is None:
if isinstance(value, unicode):
text = value
elif self.text_handler:
text = self.text_handler(value)
assert isinstance(text, unicode)
else:
text = makeUnicode(value)
return DataValue(value, text)
def add(self, value):
if isinstance(value, tuple):
if len(value) != 2:
raise ValueError("Data.add() only accept tuple of 2 elements: (value,text)")
value, text = value
else:
text = None
# Skip value 'None'
if value is None:
return
if isinstance(value, (str, unicode)):
value = normalizeString(value)
if not value:
return
# Convert string to Unicode string using charset ISO-8859-1
if self.conversion:
try:
new_value = self.conversion(self.metadata, self.key, value)
except HACHOIR_ERRORS, err:
self.metadata.warning("Error during conversion of %r value: %s" % (
self.key, err))
return
if new_value is None:
dest_types = " or ".join(str(item.__name__) for item in self.type)
self.metadata.warning("Unable to convert %s=%r (%s) to %s" % (
self.key, value, type(value).__name__, dest_types))
return
if isinstance(new_value, tuple):
if text:
value = new_value[0]
else:
value, text = new_value
else:
value = new_value
elif isinstance(value, str):
value = unicode(value, "ISO-8859-1")
if self.type and not isinstance(value, self.type):
dest_types = " or ".join(str(item.__name__) for item in self.type)
self.metadata.warning("Key %r: value %r type (%s) is not %s" % (
self.key, value, type(value).__name__, dest_types))
return
# Skip empty strings
if isinstance(value, unicode):
value = normalizeNewline(value)
if config.MAX_STR_LENGTH \
and config.MAX_STR_LENGTH < len(value):
value = value[:config.MAX_STR_LENGTH] + "(...)"
# Skip duplicates
if value in self:
return
# Use filter
if self.filter and not self.filter(value):
self.metadata.warning("Skip value %s=%r (filter)" % (self.key, value))
return
# For string, if you have "verlongtext" and "verylo",
# keep the longer value
if isinstance(value, unicode):
for index, item in enumerate(self.values):
item = item.value
if not isinstance(item, unicode):
continue
if value.startswith(item):
# Find longer value, replace the old one
self.values[index] = self._createItem(value, text)
return
if item.startswith(value):
# Find truncated value, skip it
return
# Add new value
self.values.append(self._createItem(value, text))
def __len__(self):
return len(self.values)
def __getitem__(self, index):
return self.values[index]
def __contains__(self, value):
for item in self.values:
if value == item.value:
return True
return False
def __cmp__(self, other):
return cmp(self.priority, other.priority)
-262
View File
@@ -1,262 +0,0 @@
from hachoir_metadata.metadata import RootMetadata, registerExtractor
from hachoir_metadata.safe import fault_tolerant
from hachoir_parser.container import SwfFile
from hachoir_parser.misc import TorrentFile, TrueTypeFontFile, OLE2_File, PcfFile
from hachoir_core.field import isString
from hachoir_core.error import warning
from hachoir_parser import guessParser
from hachoir_metadata.setter import normalizeString
class TorrentMetadata(RootMetadata):
KEY_TO_ATTR = {
u"announce": "url",
u"comment": "comment",
u"creation_date": "creation_date",
}
INFO_TO_ATTR = {
u"length": "file_size",
u"name": "filename",
}
def extract(self, torrent):
for field in torrent[0]:
self.processRoot(field)
@fault_tolerant
def processRoot(self, field):
if field.name in self.KEY_TO_ATTR:
key = self.KEY_TO_ATTR[field.name]
value = field.value
setattr(self, key, value)
elif field.name == "info" and "value" in field:
for field in field["value"]:
self.processInfo(field)
@fault_tolerant
def processInfo(self, field):
if field.name in self.INFO_TO_ATTR:
key = self.INFO_TO_ATTR[field.name]
value = field.value
setattr(self, key, value)
elif field.name == "piece_length":
self.comment = "Piece length: %s" % field.display
class TTF_Metadata(RootMetadata):
NAMEID_TO_ATTR = {
0: "copyright", # Copyright notice
3: "title", # Unique font identifier
5: "version", # Version string
8: "author", # Manufacturer name
11: "url", # URL Vendor
14: "copyright", # License info URL
}
def extract(self, ttf):
if "header" in ttf:
self.extractHeader(ttf["header"])
if "names" in ttf:
self.extractNames(ttf["names"])
@fault_tolerant
def extractHeader(self, header):
self.creation_date = header["created"].value
self.last_modification = header["modified"].value
self.comment = u"Smallest readable size in pixels: %s pixels" % header["lowest"].value
self.comment = u"Font direction: %s" % header["font_dir"].display
@fault_tolerant
def extractNames(self, names):
offset = names["offset"].value
for header in names.array("header"):
key = header["nameID"].value
foffset = offset + header["offset"].value
field = names.getFieldByAddress(foffset*8)
if not field or not isString(field):
continue
value = field.value
if key not in self.NAMEID_TO_ATTR:
continue
key = self.NAMEID_TO_ATTR[key]
if key == "version" and value.startswith(u"Version "):
# "Version 1.2" => "1.2"
value = value[8:]
setattr(self, key, value)
class OLE2_Metadata(RootMetadata):
SUMMARY_ID_TO_ATTR = {
2: "title", # Title
3: "title", # Subject
4: "author",
6: "comment",
8: "author", # Last saved by
12: "creation_date",
13: "last_modification",
14: "nb_page",
18: "producer",
}
IGNORE_SUMMARY = set((
1, # Code page
))
DOC_SUMMARY_ID_TO_ATTR = {
3: "title", # Subject
14: "author", # Manager
}
IGNORE_DOC_SUMMARY = set((
1, # Code page
))
def extract(self, ole2):
self._extract(ole2)
def _extract(self, fieldset, main_document=True):
if main_document:
# _feedAll() is needed to make sure that we get all root[*] fragments
fieldset._feedAll()
if "root[0]" in fieldset:
self.useRoot(fieldset["root[0]"])
doc_summary = self.getField(fieldset, main_document, "doc_summary[0]")
if doc_summary:
self.useSummary(doc_summary, True)
word_doc = self.getField(fieldset, main_document, "word_doc[0]")
if word_doc:
self.useWordDocument(word_doc)
summary = self.getField(fieldset, main_document, "summary[0]")
if summary:
self.useSummary(summary, False)
@fault_tolerant
def useRoot(self, root):
stream = root.getSubIStream()
ministream = guessParser(stream)
if not ministream:
warning("Unable to create the OLE2 mini stream parser!")
return
self._extract(ministream, main_document=False)
def getField(self, fieldset, main_document, name):
if name not in fieldset:
return None
# _feedAll() is needed to make sure that we get all fragments
# eg. summary[0], summary[1], ..., summary[n]
fieldset._feedAll()
field = fieldset[name]
if main_document:
stream = field.getSubIStream()
field = guessParser(stream)
if not field:
warning("Unable to create the OLE2 parser for %s!" % name)
return None
return field
@fault_tolerant
def useSummary(self, summary, is_doc_summary):
if "os" in summary:
self.os = summary["os"].display
if "section[0]" not in summary:
return
summary = summary["section[0]"]
for property in summary.array("property_index"):
self.useProperty(summary, property, is_doc_summary)
@fault_tolerant
def useWordDocument(self, doc):
self.comment = "Encrypted: %s" % doc["fEncrypted"].value
@fault_tolerant
def useProperty(self, summary, property, is_doc_summary):
field = summary.getFieldByAddress(property["offset"].value*8)
if not field \
or "value" not in field:
return
field = field["value"]
if not field.hasValue():
return
# Get value
value = field.value
if isinstance(value, (str, unicode)):
value = normalizeString(value)
if not value:
return
# Get property identifier
prop_id = property["id"].value
if is_doc_summary:
id_to_attr = self.DOC_SUMMARY_ID_TO_ATTR
ignore = self.IGNORE_DOC_SUMMARY
else:
id_to_attr = self.SUMMARY_ID_TO_ATTR
ignore = self.IGNORE_SUMMARY
if prop_id in ignore:
return
# Get Hachoir metadata key
try:
key = id_to_attr[prop_id]
use_prefix = False
except LookupError:
key = "comment"
use_prefix = True
if use_prefix:
prefix = property["id"].display
if (prefix in ("TotalEditingTime", "LastPrinted")) \
and (not field):
# Ignore null time delta
return
value = "%s: %s" % (prefix, value)
else:
if (key == "last_modification") and (not field):
# Ignore null timestamp
return
setattr(self, key, value)
class PcfMetadata(RootMetadata):
PROP_TO_KEY = {
'CHARSET_REGISTRY': 'charset',
'COPYRIGHT': 'copyright',
'WEIGHT_NAME': 'font_weight',
'FOUNDRY': 'author',
'FONT': 'title',
'_XMBDFED_INFO': 'producer',
}
def extract(self, pcf):
if "properties" in pcf:
self.useProperties(pcf["properties"])
def useProperties(self, properties):
last = properties["total_str_length"]
offset0 = last.address + last.size
for index in properties.array("property"):
# Search name and value
value = properties.getFieldByAddress(offset0+index["value_offset"].value*8)
if not value:
continue
value = value.value
if not value:
continue
name = properties.getFieldByAddress(offset0+index["name_offset"].value*8)
if not name:
continue
name = name.value
if name not in self.PROP_TO_KEY:
warning("Skip %s=%r" % (name, value))
continue
key = self.PROP_TO_KEY[name]
setattr(self, key, value)
class SwfMetadata(RootMetadata):
def extract(self, swf):
self.height = swf["rect/ymax"].value # twips
self.width = swf["rect/xmax"].value # twips
self.format_version = "flash version %s" % swf["version"].value
self.frame_rate = swf["frame_rate"].value
self.comment = "Frame count: %s" % swf["frame_count"].value
registerExtractor(TorrentFile, TorrentMetadata)
registerExtractor(TrueTypeFontFile, TTF_Metadata)
registerExtractor(OLE2_File, OLE2_Metadata)
registerExtractor(PcfFile, PcfMetadata)
registerExtractor(SwfFile, SwfMetadata)
-100
View File
@@ -1,100 +0,0 @@
from hachoir_metadata.metadata import RootMetadata, registerExtractor
from hachoir_parser.program import ExeFile
from hachoir_metadata.safe import fault_tolerant, getValue
class ExeMetadata(RootMetadata):
KEY_TO_ATTR = {
u"ProductName": "title",
u"LegalCopyright": "copyright",
u"LegalTrademarks": "copyright",
u"LegalTrademarks1": "copyright",
u"LegalTrademarks2": "copyright",
u"CompanyName": "author",
u"BuildDate": "creation_date",
u"FileDescription": "title",
u"ProductVersion": "version",
}
SKIP_KEY = set((u"InternalName", u"OriginalFilename", u"FileVersion", u"BuildVersion"))
def extract(self, exe):
if exe.isPE():
self.extractPE(exe)
elif exe.isNE():
self.extractNE(exe)
def extractNE(self, exe):
if "ne_header" in exe:
self.useNE_Header(exe["ne_header"])
if "info" in exe:
self.useNEInfo(exe["info"])
@fault_tolerant
def useNEInfo(self, info):
for node in info.array("node"):
if node["name"].value == "StringFileInfo":
self.readVersionInfo(node["node[0]"])
def extractPE(self, exe):
# Read information from headers
if "pe_header" in exe:
self.usePE_Header(exe["pe_header"])
if "pe_opt_header" in exe:
self.usePE_OptHeader(exe["pe_opt_header"])
# Use PE resource
resource = exe.getResource()
if resource and "version_info/node[0]" in resource:
for node in resource.array("version_info/node[0]/node"):
if getValue(node, "name") == "StringFileInfo" \
and "node[0]" in node:
self.readVersionInfo(node["node[0]"])
@fault_tolerant
def useNE_Header(self, hdr):
if hdr["is_dll"].value:
self.format_version = u"New-style executable: Dynamic-link library (DLL)"
elif hdr["is_win_app"].value:
self.format_version = u"New-style executable: Windows 3.x application"
else:
self.format_version = u"New-style executable for Windows 3.x"
@fault_tolerant
def usePE_Header(self, hdr):
self.creation_date = hdr["creation_date"].value
self.comment = "CPU: %s" % hdr["cpu"].display
if hdr["is_dll"].value:
self.format_version = u"Portable Executable: Dynamic-link library (DLL)"
else:
self.format_version = u"Portable Executable: Windows application"
@fault_tolerant
def usePE_OptHeader(self, hdr):
self.comment = "Subsystem: %s" % hdr["subsystem"].display
def readVersionInfo(self, info):
values = {}
for node in info.array("node"):
if "value" not in node or "name" not in node:
continue
value = node["value"].value.strip(" \0")
if not value:
continue
key = node["name"].value
values[key] = value
if "ProductName" in values and "FileDescription" in values:
# Make sure that FileDescription is set before ProductName
# as title value
self.title = values["FileDescription"]
self.title = values["ProductName"]
del values["FileDescription"]
del values["ProductName"]
for key, value in values.iteritems():
if key in self.KEY_TO_ATTR:
setattr(self, self.KEY_TO_ATTR[key], value)
elif key not in self.SKIP_KEY:
self.comment = "%s=%s" % (key, value)
registerExtractor(ExeFile, ExeMetadata)
-64
View File
@@ -1,64 +0,0 @@
<ui version="4.0" >
<class>Form</class>
<widget class="QWidget" name="Form" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>441</width>
<height>412</height>
</rect>
</property>
<property name="windowTitle" >
<string>hachoir-metadata</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" >
<item>
<widget class="QPushButton" name="open_button" >
<property name="text" >
<string>Open</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="files_combo" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="metadata_table" >
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
<property name="rowCount" >
<number>0</number>
</property>
<property name="columnCount" >
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="quit_button" >
<property name="text" >
<string>Quit</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
-112
View File
@@ -1,112 +0,0 @@
from hachoir_core.i18n import _
from hachoir_core.tools import (
humanDuration, humanBitRate,
humanFrequency, humanBitSize, humanFilesize,
humanDatetime)
from hachoir_core.language import Language
from hachoir_metadata.filter import Filter, NumberFilter, DATETIME_FILTER
from datetime import date, datetime, timedelta
from hachoir_metadata.formatter import (
humanAudioChannel, humanFrameRate, humanComprRate, humanAltitude,
humanPixelSize, humanDPI)
from hachoir_metadata.setter import (
setDatetime, setTrackNumber, setTrackTotal, setLanguage)
from hachoir_metadata.metadata_item import Data
MIN_SAMPLE_RATE = 1000 # 1 kHz
MAX_SAMPLE_RATE = 192000 # 192 kHz
MAX_NB_CHANNEL = 8 # 8 channels
MAX_WIDTH = 20000 # 20 000 pixels
MAX_BIT_RATE = 500 * 1024 * 1024 # 500 Mbit/s
MAX_HEIGHT = MAX_WIDTH
MAX_DPI_WIDTH = 10000
MAX_DPI_HEIGHT = MAX_DPI_WIDTH
MAX_NB_COLOR = 2 ** 24 # 16 million of color
MAX_BITS_PER_PIXEL = 256 # 256 bits/pixel
MAX_FRAME_RATE = 150 # 150 frame/sec
MAX_NB_PAGE = 20000
MAX_COMPR_RATE = 1000.0
MIN_COMPR_RATE = 0.001
MAX_TRACK = 999
DURATION_FILTER = Filter(timedelta,
timedelta(milliseconds=1),
timedelta(days=365))
def registerAllItems(meta):
meta.register(Data("title", 100, _("Title"), type=unicode))
meta.register(Data("artist", 101, _("Artist"), type=unicode))
meta.register(Data("author", 102, _("Author"), type=unicode))
meta.register(Data("music_composer", 103, _("Music composer"), type=unicode))
meta.register(Data("album", 200, _("Album"), type=unicode))
meta.register(Data("duration", 201, _("Duration"), # integer in milliseconde
type=timedelta, text_handler=humanDuration, filter=DURATION_FILTER))
meta.register(Data("nb_page", 202, _("Nb page"), filter=NumberFilter(1, MAX_NB_PAGE)))
meta.register(Data("music_genre", 203, _("Music genre"), type=unicode))
meta.register(Data("language", 204, _("Language"), conversion=setLanguage, type=Language))
meta.register(Data("track_number", 205, _("Track number"), conversion=setTrackNumber,
filter=NumberFilter(1, MAX_TRACK), type=(int, long)))
meta.register(Data("track_total", 206, _("Track total"), conversion=setTrackTotal,
filter=NumberFilter(1, MAX_TRACK), type=(int, long)))
meta.register(Data("organization", 210, _("Organization"), type=unicode))
meta.register(Data("version", 220, _("Version")))
meta.register(Data("width", 301, _("Image width"), filter=NumberFilter(1, MAX_WIDTH), type=(int, long), text_handler=humanPixelSize))
meta.register(Data("height", 302, _("Image height"), filter=NumberFilter(1, MAX_HEIGHT), type=(int, long), text_handler=humanPixelSize))
meta.register(Data("nb_channel", 303, _("Channel"), text_handler=humanAudioChannel, filter=NumberFilter(1, MAX_NB_CHANNEL), type=(int, long)))
meta.register(Data("sample_rate", 304, _("Sample rate"), text_handler=humanFrequency, filter=NumberFilter(MIN_SAMPLE_RATE, MAX_SAMPLE_RATE), type=(int, long, float)))
meta.register(Data("bits_per_sample", 305, _("Bits/sample"), text_handler=humanBitSize, filter=NumberFilter(1, 64), type=(int, long)))
meta.register(Data("image_orientation", 306, _("Image orientation")))
meta.register(Data("nb_colors", 307, _("Number of colors"), filter=NumberFilter(1, MAX_NB_COLOR), type=(int, long)))
meta.register(Data("bits_per_pixel", 308, _("Bits/pixel"), filter=NumberFilter(1, MAX_BITS_PER_PIXEL), type=(int, long)))
meta.register(Data("filename", 309, _("File name"), type=unicode))
meta.register(Data("file_size", 310, _("File size"), text_handler=humanFilesize, type=(int, long)))
meta.register(Data("pixel_format", 311, _("Pixel format")))
meta.register(Data("compr_size", 312, _("Compressed file size"), text_handler=humanFilesize, type=(int, long)))
meta.register(Data("compr_rate", 313, _("Compression rate"), text_handler=humanComprRate, filter=NumberFilter(MIN_COMPR_RATE, MAX_COMPR_RATE), type=(int, long, float)))
meta.register(Data("width_dpi", 320, _("Image DPI width"), filter=NumberFilter(1, MAX_DPI_WIDTH), type=(int, long), text_handler=humanDPI))
meta.register(Data("height_dpi", 321, _("Image DPI height"), filter=NumberFilter(1, MAX_DPI_HEIGHT), type=(int, long), text_handler=humanDPI))
meta.register(Data("file_attr", 400, _("File attributes")))
meta.register(Data("file_type", 401, _("File type")))
meta.register(Data("subtitle_author", 402, _("Subtitle author"), type=unicode))
meta.register(Data("creation_date", 500, _("Creation date"), text_handler=humanDatetime,
filter=DATETIME_FILTER, type=(datetime, date), conversion=setDatetime))
meta.register(Data("last_modification", 501, _("Last modification"), text_handler=humanDatetime,
filter=DATETIME_FILTER, type=(datetime, date), conversion=setDatetime))
meta.register(Data("latitude", 510, _("Latitude"), type=float))
meta.register(Data("longitude", 511, _("Longitude"), type=float))
meta.register(Data("altitude", 511, _("Altitude"), type=float, text_handler=humanAltitude))
meta.register(Data("location", 530, _("Location"), type=unicode))
meta.register(Data("city", 531, _("City"), type=unicode))
meta.register(Data("country", 532, _("Country"), type=unicode))
meta.register(Data("charset", 540, _("Charset"), type=unicode))
meta.register(Data("font_weight", 550, _("Font weight")))
meta.register(Data("camera_aperture", 520, _("Camera aperture")))
meta.register(Data("camera_focal", 521, _("Camera focal")))
meta.register(Data("camera_exposure", 522, _("Camera exposure")))
meta.register(Data("camera_brightness", 530, _("Camera brightness")))
meta.register(Data("camera_model", 531, _("Camera model"), type=unicode))
meta.register(Data("camera_manufacturer", 532, _("Camera manufacturer"), type=unicode))
meta.register(Data("compression", 600, _("Compression")))
meta.register(Data("copyright", 601, _("Copyright"), type=unicode))
meta.register(Data("url", 602, _("URL"), type=unicode))
meta.register(Data("frame_rate", 603, _("Frame rate"), text_handler=humanFrameRate,
filter=NumberFilter(1, MAX_FRAME_RATE), type=(int, long, float)))
meta.register(Data("bit_rate", 604, _("Bit rate"), text_handler=humanBitRate,
filter=NumberFilter(1, MAX_BIT_RATE), type=(int, long, float)))
meta.register(Data("aspect_ratio", 604, _("Aspect ratio"), type=(int, long, float)))
meta.register(Data("os", 900, _("OS"), type=unicode))
meta.register(Data("producer", 901, _("Producer"), type=unicode))
meta.register(Data("comment", 902, _("Comment"), type=unicode))
meta.register(Data("format_version", 950, _("Format version"), type=unicode))
meta.register(Data("mime_type", 951, _("MIME type"), type=unicode))
meta.register(Data("endian", 952, _("Endianness"), type=unicode))
-190
View File
@@ -1,190 +0,0 @@
"""
Extract metadata from RIFF file format: AVI video and WAV sound.
"""
from hachoir_metadata.metadata import Metadata, MultipleMetadata, registerExtractor
from hachoir_metadata.safe import fault_tolerant, getValue
from hachoir_parser.container.riff import RiffFile
from hachoir_parser.video.fourcc import UNCOMPRESSED_AUDIO
from hachoir_core.tools import humanFilesize, makeUnicode, timedelta2seconds
from hachoir_core.i18n import _
from hachoir_metadata.audio import computeComprRate as computeAudioComprRate
from datetime import timedelta
class RiffMetadata(MultipleMetadata):
TAG_TO_KEY = {
"INAM": "title",
"IART": "artist",
"ICMT": "comment",
"ICOP": "copyright",
"IENG": "author", # (engineer)
"ISFT": "producer",
"ICRD": "creation_date",
"IDIT": "creation_date",
}
def extract(self, riff):
type = riff["type"].value
if type == "WAVE":
self.extractWAVE(riff)
size = getValue(riff, "audio_data/size")
if size:
computeAudioComprRate(self, size*8)
elif type == "AVI ":
if "headers" in riff:
self.extractAVI(riff["headers"])
self.extractInfo(riff["headers"])
elif type == "ACON":
self.extractAnim(riff)
if "info" in riff:
self.extractInfo(riff["info"])
def processChunk(self, chunk):
if "text" not in chunk:
return
value = chunk["text"].value
tag = chunk["tag"].value
if tag not in self.TAG_TO_KEY:
self.warning("Skip RIFF metadata %s: %s" % (tag, value))
return
key = self.TAG_TO_KEY[tag]
setattr(self, key, value)
@fault_tolerant
def extractWAVE(self, wav):
format = wav["format"]
# Number of channel, bits/sample, sample rate
self.nb_channel = format["nb_channel"].value
self.bits_per_sample = format["bit_per_sample"].value
self.sample_rate = format["sample_per_sec"].value
self.compression = format["codec"].display
if "nb_sample/nb_sample" in wav \
and 0 < format["sample_per_sec"].value:
self.duration = timedelta(seconds=float(wav["nb_sample/nb_sample"].value) / format["sample_per_sec"].value)
if format["codec"].value in UNCOMPRESSED_AUDIO:
# Codec with fixed bit rate
self.bit_rate = format["nb_channel"].value * format["bit_per_sample"].value * format["sample_per_sec"].value
if not self.has("duration") \
and "audio_data/size" in wav \
and self.has("bit_rate"):
duration = float(wav["audio_data/size"].value)*8 / self.get('bit_rate')
self.duration = timedelta(seconds=duration)
def extractInfo(self, fieldset):
for field in fieldset:
if not field.is_field_set:
continue
if "tag" in field:
if field["tag"].value == "LIST":
self.extractInfo(field)
else:
self.processChunk(field)
@fault_tolerant
def extractAVIVideo(self, header, meta):
meta.compression = "%s (fourcc:\"%s\")" \
% (header["fourcc"].display, makeUnicode(header["fourcc"].value))
if header["rate"].value and header["scale"].value:
fps = float(header["rate"].value) / header["scale"].value
meta.frame_rate = fps
if 0 < fps:
self.duration = meta.duration = timedelta(seconds=float(header["length"].value) / fps)
if "../stream_fmt/width" in header:
format = header["../stream_fmt"]
meta.width = format["width"].value
meta.height = format["height"].value
meta.bits_per_pixel = format["depth"].value
else:
meta.width = header["right"].value - header["left"].value
meta.height = header["bottom"].value - header["top"].value
@fault_tolerant
def extractAVIAudio(self, format, meta):
meta.nb_channel = format["channel"].value
meta.sample_rate = format["sample_rate"].value
meta.bit_rate = format["bit_rate"].value * 8
if format["bits_per_sample"].value:
meta.bits_per_sample = format["bits_per_sample"].value
if "../stream_hdr" in format:
header = format["../stream_hdr"]
if header["rate"].value and header["scale"].value:
frame_rate = float(header["rate"].value) / header["scale"].value
meta.duration = timedelta(seconds=float(header["length"].value) / frame_rate)
if header["fourcc"].value != "":
meta.compression = "%s (fourcc:\"%s\")" \
% (format["codec"].display, header["fourcc"].value)
if not meta.has("compression"):
meta.compression = format["codec"].display
self.computeAudioComprRate(meta)
@fault_tolerant
def computeAudioComprRate(self, meta):
uncompr = meta.get('bit_rate', 0)
if not uncompr:
return
compr = meta.get('nb_channel') * meta.get('sample_rate') * meta.get('bits_per_sample', default=16)
if not compr:
return
meta.compr_rate = float(compr) / uncompr
@fault_tolerant
def useAviHeader(self, header):
microsec = header["microsec_per_frame"].value
if microsec:
self.frame_rate = 1000000.0 / microsec
total_frame = getValue(header, "total_frame")
if total_frame and not self.has("duration"):
self.duration = timedelta(microseconds=total_frame * microsec)
self.width = header["width"].value
self.height = header["height"].value
def extractAVI(self, headers):
audio_index = 1
for stream in headers.array("stream"):
if "stream_hdr/stream_type" not in stream:
continue
stream_type = stream["stream_hdr/stream_type"].value
if stream_type == "vids":
if "stream_hdr" in stream:
meta = Metadata(self)
self.extractAVIVideo(stream["stream_hdr"], meta)
self.addGroup("video", meta, "Video stream")
elif stream_type == "auds":
if "stream_fmt" in stream:
meta = Metadata(self)
self.extractAVIAudio(stream["stream_fmt"], meta)
self.addGroup("audio[%u]" % audio_index, meta, "Audio stream")
audio_index += 1
if "avi_hdr" in headers:
self.useAviHeader(headers["avi_hdr"])
# Compute global bit rate
if self.has("duration") and "/movie/size" in headers:
self.bit_rate = float(headers["/movie/size"].value) * 8 / timedelta2seconds(self.get('duration'))
# Video has index?
if "/index" in headers:
self.comment = _("Has audio/video index (%s)") \
% humanFilesize(headers["/index"].size/8)
@fault_tolerant
def extractAnim(self, riff):
if "anim_rate/rate[0]" in riff:
count = 0
total = 0
for rate in riff.array("anim_rate/rate"):
count += 1
if 100 < count:
break
total += rate.value / 60.0
if count and total:
self.frame_rate = count / total
if not self.has("frame_rate") and "anim_hdr/jiffie_rate" in riff:
self.frame_rate = 60.0 / riff["anim_hdr/jiffie_rate"].value
registerExtractor(RiffFile, RiffMetadata)
-27
View File
@@ -1,27 +0,0 @@
from hachoir_core.error import HACHOIR_ERRORS, warning
def fault_tolerant(func, *args):
def safe_func(*args, **kw):
try:
func(*args, **kw)
except HACHOIR_ERRORS, err:
warning("Error when calling function %s(): %s" % (
func.__name__, err))
return safe_func
def getFieldAttribute(fieldset, key, attrname):
try:
field = fieldset[key]
if field.hasValue():
return getattr(field, attrname)
except HACHOIR_ERRORS, err:
warning("Unable to get %s of field %s/%s: %s" % (
attrname, fieldset.path, key, err))
return None
def getValue(fieldset, key):
return getFieldAttribute(fieldset, key, "value")
def getDisplay(fieldset, key):
return getFieldAttribute(fieldset, key, "display")
-171
View File
@@ -1,171 +0,0 @@
from datetime import date, datetime
import re
from hachoir_core.language import Language
from locale import setlocale, LC_ALL
from time import strptime
from hachoir_metadata.timezone import createTimezone
from hachoir_metadata import config
NORMALIZE_REGEX = re.compile("[-/.: ]+")
YEAR_REGEX1 = re.compile("^([0-9]{4})$")
# Date regex: YYYY-MM-DD (US format)
DATE_REGEX1 = re.compile("^([0-9]{4})~([01][0-9])~([0-9]{2})$")
# Date regex: YYYY-MM-DD HH:MM:SS (US format)
DATETIME_REGEX1 = re.compile("^([0-9]{4})~([01][0-9])~([0-9]{2})~([0-9]{1,2})~([0-9]{2})~([0-9]{2})$")
# Datetime regex: "MM-DD-YYYY HH:MM:SS" (FR format)
DATETIME_REGEX2 = re.compile("^([01]?[0-9])~([0-9]{2})~([0-9]{4})~([0-9]{1,2})~([0-9]{2})~([0-9]{2})$")
# Timezone regex: "(...) +0200"
TIMEZONE_REGEX = re.compile("^(.*)~([+-][0-9]{2})00$")
# Timestmap: 'February 2007'
MONTH_YEAR = "%B~%Y"
# Timestmap: 'Sun Feb 24 15:51:09 2008'
RIFF_TIMESTAMP = "%a~%b~%d~%H~%M~%S~%Y"
# Timestmap: 'Thu, 19 Jul 2007 09:03:57'
ISO_TIMESTAMP = "%a,~%d~%b~%Y~%H~%M~%S"
def parseDatetime(value):
"""
Year and date:
>>> parseDatetime("2000")
(datetime.date(2000, 1, 1), u'2000')
>>> parseDatetime("2004-01-02")
datetime.date(2004, 1, 2)
Timestamp:
>>> parseDatetime("2004-01-02 18:10:45")
datetime.datetime(2004, 1, 2, 18, 10, 45)
>>> parseDatetime("2004-01-02 18:10:45")
datetime.datetime(2004, 1, 2, 18, 10, 45)
Timestamp with timezone:
>>> parseDatetime(u'Thu, 19 Jul 2007 09:03:57 +0000')
datetime.datetime(2007, 7, 19, 9, 3, 57, tzinfo=<TimezoneUTC delta=0, name=u'UTC'>)
>>> parseDatetime(u'Thu, 19 Jul 2007 09:03:57 +0200')
datetime.datetime(2007, 7, 19, 9, 3, 57, tzinfo=<Timezone delta=2:00:00, name='+0200'>)
"""
value = NORMALIZE_REGEX.sub("~", value.strip())
regs = YEAR_REGEX1.match(value)
if regs:
try:
year = int(regs.group(1))
return (date(year, 1, 1), unicode(year))
except ValueError:
pass
regs = DATE_REGEX1.match(value)
if regs:
try:
year = int(regs.group(1))
month = int(regs.group(2))
day = int(regs.group(3))
return date(year, month, day)
except ValueError:
pass
regs = DATETIME_REGEX1.match(value)
if regs:
try:
year = int(regs.group(1))
month = int(regs.group(2))
day = int(regs.group(3))
hour = int(regs.group(4))
min = int(regs.group(5))
sec = int(regs.group(6))
return datetime(year, month, day, hour, min, sec)
except ValueError:
pass
regs = DATETIME_REGEX2.match(value)
if regs:
try:
month = int(regs.group(1))
day = int(regs.group(2))
year = int(regs.group(3))
hour = int(regs.group(4))
min = int(regs.group(5))
sec = int(regs.group(6))
return datetime(year, month, day, hour, min, sec)
except ValueError:
pass
current_locale = setlocale(LC_ALL, "C")
try:
match = TIMEZONE_REGEX.match(value)
if match:
without_timezone = match.group(1)
delta = int(match.group(2))
delta = createTimezone(delta)
else:
without_timezone = value
delta = None
try:
timestamp = strptime(without_timezone, ISO_TIMESTAMP)
arguments = list(timestamp[0:6]) + [0, delta]
return datetime(*arguments)
except ValueError:
pass
try:
timestamp = strptime(without_timezone, RIFF_TIMESTAMP)
arguments = list(timestamp[0:6]) + [0, delta]
return datetime(*arguments)
except ValueError:
pass
try:
timestamp = strptime(value, MONTH_YEAR)
arguments = list(timestamp[0:3])
return date(*arguments)
except ValueError:
pass
finally:
setlocale(LC_ALL, current_locale)
return None
def setDatetime(meta, key, value):
if isinstance(value, (str, unicode)):
return parseDatetime(value)
elif isinstance(value, (date, datetime)):
return value
return None
def setLanguage(meta, key, value):
"""
>>> setLanguage(None, None, "fre")
<Language 'French', code='fre'>
>>> setLanguage(None, None, u"ger")
<Language 'German', code='ger'>
"""
return Language(value)
def setTrackTotal(meta, key, total):
"""
>>> setTrackTotal(None, None, "10")
10
"""
try:
return int(total)
except ValueError:
meta.warning("Invalid track total: %r" % total)
return None
def setTrackNumber(meta, key, number):
if isinstance(number, (int, long)):
return number
if "/" in number:
number, total = number.split("/", 1)
meta.track_total = total
try:
return int(number)
except ValueError:
meta.warning("Invalid track number: %r" % number)
return None
def normalizeString(text):
if config.RAW_OUTPUT:
return text
return text.strip(" \t\v\n\r\0")
-42
View File
@@ -1,42 +0,0 @@
from datetime import tzinfo, timedelta
class TimezoneUTC(tzinfo):
"""UTC timezone"""
ZERO = timedelta(0)
def utcoffset(self, dt):
return TimezoneUTC.ZERO
def tzname(self, dt):
return u"UTC"
def dst(self, dt):
return TimezoneUTC.ZERO
def __repr__(self):
return "<TimezoneUTC delta=0, name=u'UTC'>"
class Timezone(TimezoneUTC):
"""Fixed offset in hour from UTC."""
def __init__(self, offset):
self._offset = timedelta(minutes=offset*60)
self._name = u"%+03u00" % offset
def utcoffset(self, dt):
return self._offset
def tzname(self, dt):
return self._name
def __repr__(self):
return "<Timezone delta=%s, name='%s'>" % (
self._offset, self._name)
UTC = TimezoneUTC()
def createTimezone(offset):
if offset:
return Timezone(offset)
else:
return UTC
-5
View File
@@ -1,5 +0,0 @@
PACKAGE = "hachoir-metadata"
VERSION = "1.3.3"
WEBSITE = "http://bitbucket.org/haypo/hachoir/wiki/hachoir-metadata"
LICENSE = "GNU GPL v2"
-412
View File
@@ -1,412 +0,0 @@
from hachoir_core.field import MissingField
from hachoir_metadata.metadata import (registerExtractor,
Metadata, RootMetadata, MultipleMetadata)
from hachoir_metadata.metadata_item import QUALITY_GOOD
from hachoir_metadata.safe import fault_tolerant
from hachoir_parser.video import MovFile, AsfFile, FlvFile
from hachoir_parser.video.asf import Descriptor as ASF_Descriptor
from hachoir_parser.container import MkvFile
from hachoir_parser.container.mkv import dateToDatetime
from hachoir_core.i18n import _
from hachoir_core.tools import makeUnicode, makePrintable, timedelta2seconds
from datetime import timedelta
class MkvMetadata(MultipleMetadata):
tag_key = {
"TITLE": "title",
"URL": "url",
"COPYRIGHT": "copyright",
# TODO: use maybe another name?
# Its value may be different than (...)/Info/DateUTC/date
"DATE_RECORDED": "creation_date",
# TODO: Extract subtitle metadata
"SUBTITLE": "subtitle_author",
}
def extract(self, mkv):
for segment in mkv.array("Segment"):
self.processSegment(segment)
def processSegment(self, segment):
for field in segment:
if field.name.startswith("Info["):
self.processInfo(field)
elif field.name.startswith("Tags["):
for tag in field.array("Tag"):
self.processTag(tag)
elif field.name.startswith("Tracks["):
self.processTracks(field)
elif field.name.startswith("Cluster["):
if self.quality < QUALITY_GOOD:
return
def processTracks(self, tracks):
for entry in tracks.array("TrackEntry"):
self.processTrack(entry)
def processTrack(self, track):
if "TrackType/enum" not in track:
return
if track["TrackType/enum"].display == "video":
self.processVideo(track)
elif track["TrackType/enum"].display == "audio":
self.processAudio(track)
elif track["TrackType/enum"].display == "subtitle":
self.processSubtitle(track)
def trackCommon(self, track, meta):
if "Name/unicode" in track:
meta.title = track["Name/unicode"].value
if "Language/string" in track \
and track["Language/string"].value not in ("mis", "und"):
meta.language = track["Language/string"].value
def processVideo(self, track):
video = Metadata(self)
self.trackCommon(track, video)
try:
video.compression = track["CodecID/string"].value
if "Video" in track:
video.width = track["Video/PixelWidth/unsigned"].value
video.height = track["Video/PixelHeight/unsigned"].value
except MissingField:
pass
self.addGroup("video[]", video, "Video stream")
def getDouble(self, field, parent):
float_key = '%s/float' % parent
if float_key in field:
return field[float_key].value
double_key = '%s/double' % parent
if double_key in field:
return field[double_key].value
return None
def processAudio(self, track):
audio = Metadata(self)
self.trackCommon(track, audio)
if "Audio" in track:
frequency = self.getDouble(track, "Audio/SamplingFrequency")
if frequency is not None:
audio.sample_rate = frequency
if "Audio/Channels/unsigned" in track:
audio.nb_channel = track["Audio/Channels/unsigned"].value
if "Audio/BitDepth/unsigned" in track:
audio.bits_per_sample = track["Audio/BitDepth/unsigned"].value
if "CodecID/string" in track:
audio.compression = track["CodecID/string"].value
self.addGroup("audio[]", audio, "Audio stream")
def processSubtitle(self, track):
sub = Metadata(self)
self.trackCommon(track, sub)
try:
sub.compression = track["CodecID/string"].value
except MissingField:
pass
self.addGroup("subtitle[]", sub, "Subtitle")
def processTag(self, tag):
for field in tag.array("SimpleTag"):
self.processSimpleTag(field)
def processSimpleTag(self, tag):
if "TagName/unicode" not in tag \
or "TagString/unicode" not in tag:
return
name = tag["TagName/unicode"].value
if name not in self.tag_key:
return
key = self.tag_key[name]
value = tag["TagString/unicode"].value
setattr(self, key, value)
def processInfo(self, info):
if "TimecodeScale/unsigned" in info:
duration = self.getDouble(info, "Duration")
if duration is not None:
try:
seconds = duration * info["TimecodeScale/unsigned"].value * 1e-9
self.duration = timedelta(seconds=seconds)
except OverflowError:
# Catch OverflowError for timedelta (long int too large
# to be converted to an int)
pass
if "DateUTC/date" in info:
try:
self.creation_date = dateToDatetime(info["DateUTC/date"].value)
except OverflowError:
pass
if "WritingApp/unicode" in info:
self.producer = info["WritingApp/unicode"].value
if "MuxingApp/unicode" in info:
self.producer = info["MuxingApp/unicode"].value
if "Title/unicode" in info:
self.title = info["Title/unicode"].value
class FlvMetadata(MultipleMetadata):
def extract(self, flv):
if "video[0]" in flv:
meta = Metadata(self)
self.extractVideo(flv["video[0]"], meta)
self.addGroup("video", meta, "Video stream")
if "audio[0]" in flv:
meta = Metadata(self)
self.extractAudio(flv["audio[0]"], meta)
self.addGroup("audio", meta, "Audio stream")
# TODO: Computer duration
# One technic: use last video/audio chunk and use timestamp
# But this is very slow
self.format_version = flv.description
if "metadata/entry[1]" in flv:
self.extractAMF(flv["metadata/entry[1]"])
if self.has('duration'):
self.bit_rate = flv.size / timedelta2seconds(self.get('duration'))
@fault_tolerant
def extractAudio(self, audio, meta):
if audio["codec"].display == "MP3" and "music_data" in audio:
meta.compression = audio["music_data"].description
else:
meta.compression = audio["codec"].display
meta.sample_rate = audio.getSampleRate()
if audio["is_16bit"].value:
meta.bits_per_sample = 16
else:
meta.bits_per_sample = 8
if audio["is_stereo"].value:
meta.nb_channel = 2
else:
meta.nb_channel = 1
@fault_tolerant
def extractVideo(self, video, meta):
meta.compression = video["codec"].display
def extractAMF(self, amf):
for entry in amf.array("item"):
self.useAmfEntry(entry)
@fault_tolerant
def useAmfEntry(self, entry):
key = entry["key"].value
if key == "duration":
self.duration = timedelta(seconds=entry["value"].value)
elif key == "creator":
self.producer = entry["value"].value
elif key == "audiosamplerate":
self.sample_rate = entry["value"].value
elif key == "framerate":
self.frame_rate = entry["value"].value
elif key == "metadatacreator":
self.producer = entry["value"].value
elif key == "metadatadate":
self.creation_date = entry.value
elif key == "width":
self.width = int(entry["value"].value)
elif key == "height":
self.height = int(entry["value"].value)
class MovMetadata(RootMetadata):
def extract(self, mov):
for atom in mov:
if "movie" in atom:
self.processMovie(atom["movie"])
@fault_tolerant
def processMovieHeader(self, hdr):
self.creation_date = hdr["creation_date"].value
self.last_modification = hdr["lastmod_date"].value
self.duration = timedelta(seconds=float(hdr["duration"].value) / hdr["time_scale"].value)
self.comment = _("Play speed: %.1f%%") % (hdr["play_speed"].value*100)
self.comment = _("User volume: %.1f%%") % (float(hdr["volume"].value)*100//255)
@fault_tolerant
def processTrackHeader(self, hdr):
width = int(hdr["frame_size_width"].value)
height = int(hdr["frame_size_height"].value)
if width and height:
self.width = width
self.height = height
def processTrack(self, atom):
for field in atom:
if "track_hdr" in field:
self.processTrackHeader(field["track_hdr"])
def processMovie(self, atom):
for field in atom:
if "track" in field:
self.processTrack(field["track"])
if "movie_hdr" in field:
self.processMovieHeader(field["movie_hdr"])
class AsfMetadata(MultipleMetadata):
EXT_DESC_TO_ATTR = {
"Encoder": "producer",
"ToolName": "producer",
"AlbumTitle": "album",
"Track": "track_number",
"TrackNumber": "track_total",
"Year": "creation_date",
"AlbumArtist": "author",
}
SKIP_EXT_DESC = set((
# Useless informations
"WMFSDKNeeded", "WMFSDKVersion",
"Buffer Average", "VBR Peak", "EncodingTime",
"MediaPrimaryClassID", "UniqueFileIdentifier",
))
def extract(self, asf):
if "header/content" in asf:
self.processHeader(asf["header/content"])
def processHeader(self, header):
compression = []
is_vbr = None
if "ext_desc/content" in header:
# Extract all data from ext_desc
data = {}
for desc in header.array("ext_desc/content/descriptor"):
self.useExtDescItem(desc, data)
# Have ToolName and ToolVersion? If yes, group them to producer key
if "ToolName" in data and "ToolVersion" in data:
self.producer = "%s (version %s)" % (data["ToolName"], data["ToolVersion"])
del data["ToolName"]
del data["ToolVersion"]
# "IsVBR" key
if "IsVBR" in data:
is_vbr = (data["IsVBR"] == 1)
del data["IsVBR"]
# Store data
for key, value in data.iteritems():
if key in self.EXT_DESC_TO_ATTR:
key = self.EXT_DESC_TO_ATTR[key]
else:
if isinstance(key, str):
key = makePrintable(key, "ISO-8859-1", to_unicode=True)
value = "%s=%s" % (key, value)
key = "comment"
setattr(self, key, value)
if "file_prop/content" in header:
self.useFileProp(header["file_prop/content"], is_vbr)
if "codec_list/content" in header:
for codec in header.array("codec_list/content/codec"):
if "name" in codec:
text = codec["name"].value
if "desc" in codec and codec["desc"].value:
text = "%s (%s)" % (text, codec["desc"].value)
compression.append(text)
audio_index = 1
video_index = 1
for index, stream_prop in enumerate(header.array("stream_prop")):
if "content/audio_header" in stream_prop:
meta = Metadata(self)
self.streamProperty(header, index, meta)
self.streamAudioHeader(stream_prop["content/audio_header"], meta)
if self.addGroup("audio[%u]" % audio_index, meta, "Audio stream #%u" % audio_index):
audio_index += 1
elif "content/video_header" in stream_prop:
meta = Metadata(self)
self.streamProperty(header, index, meta)
self.streamVideoHeader(stream_prop["content/video_header"], meta)
if self.addGroup("video[%u]" % video_index, meta, "Video stream #%u" % video_index):
video_index += 1
if "metadata/content" in header:
info = header["metadata/content"]
try:
self.title = info["title"].value
self.author = info["author"].value
self.copyright = info["copyright"].value
except MissingField:
pass
@fault_tolerant
def streamAudioHeader(self, audio, meta):
if not meta.has("compression"):
meta.compression = audio["twocc"].display
meta.nb_channel = audio["channels"].value
meta.sample_rate = audio["sample_rate"].value
meta.bits_per_sample = audio["bits_per_sample"].value
@fault_tolerant
def streamVideoHeader(self, video, meta):
meta.width = video["width"].value
meta.height = video["height"].value
if "bmp_info" in video:
bmp_info = video["bmp_info"]
if not meta.has("compression"):
meta.compression = bmp_info["codec"].display
meta.bits_per_pixel = bmp_info["bpp"].value
@fault_tolerant
def useExtDescItem(self, desc, data):
if desc["type"].value == ASF_Descriptor.TYPE_BYTE_ARRAY:
# Skip binary data
return
key = desc["name"].value
if "/" in key:
# Replace "WM/ToolName" with "ToolName"
key = key.split("/", 1)[1]
if key in self.SKIP_EXT_DESC:
# Skip some keys
return
value = desc["value"].value
if not value:
return
value = makeUnicode(value)
data[key] = value
@fault_tolerant
def useFileProp(self, prop, is_vbr):
self.creation_date = prop["creation_date"].value
self.duration = prop["play_duration"].value
if prop["seekable"].value:
self.comment = u"Is seekable"
value = prop["max_bitrate"].value
text = prop["max_bitrate"].display
if is_vbr is True:
text = "VBR (%s max)" % text
elif is_vbr is False:
text = "%s (CBR)" % text
else:
text = "%s (max)" % text
self.bit_rate = (value, text)
def streamProperty(self, header, index, meta):
key = "bit_rates/content/bit_rate[%u]/avg_bitrate" % index
if key in header:
meta.bit_rate = header[key].value
# TODO: Use codec list
# It doesn't work when the video uses /header/content/bitrate_mutex
# since the codec list are shared between streams but... how is it
# shared?
# key = "codec_list/content/codec[%u]" % index
# if key in header:
# codec = header[key]
# if "name" in codec:
# text = codec["name"].value
# if "desc" in codec and codec["desc"].value:
# meta.compression = "%s (%s)" % (text, codec["desc"].value)
# else:
# meta.compression = text
registerExtractor(MovFile, MovMetadata)
registerExtractor(AsfFile, AsfMetadata)
registerExtractor(FlvFile, FlvMetadata)
registerExtractor(MkvFile, MkvMetadata)
-7
View File
@@ -1,7 +0,0 @@
from hachoir_parser.version import __version__
from hachoir_parser.parser import ValidateError, HachoirParser, Parser
from hachoir_parser.parser_list import ParserList, HachoirParserList
from hachoir_parser.guess import (QueryParser, guessParser, createParser)
from hachoir_parser import (archive, audio, container,
file_system, image, game, misc, network, program, video)
-12
View File
@@ -1,12 +0,0 @@
from hachoir_parser.archive.ace import AceFile
from hachoir_parser.archive.ar import ArchiveFile
from hachoir_parser.archive.bzip2_parser import Bzip2Parser
from hachoir_parser.archive.cab import CabFile
from hachoir_parser.archive.gzip_parser import GzipParser
from hachoir_parser.archive.tar import TarFile
from hachoir_parser.archive.zip import ZipFile
from hachoir_parser.archive.rar import RarFile
from hachoir_parser.archive.rpm import RpmFile
from hachoir_parser.archive.sevenzip import SevenZipParser
from hachoir_parser.archive.mar import MarFile
-267
View File
@@ -1,267 +0,0 @@
"""
ACE parser
From wotsit.org and the SDK header (bitflags)
Partial study of a new block type (5) I've called "new_recovery", as its
syntax is very close to the former one (of type 2).
Status: can only read totally file and header blocks.
Author: Christophe Gisquet <christophe.gisquet@free.fr>
Creation date: 19 january 2006
"""
from hachoir_parser import Parser
from hachoir_core.field import (StaticFieldSet, FieldSet,
Bit, Bits, NullBits, RawBytes, Enum,
UInt8, UInt16, UInt32,
PascalString8, PascalString16, String,
TimeDateMSDOS32)
from hachoir_core.text_handler import textHandler, filesizeHandler, hexadecimal
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_parser.common.msdos import MSDOSFileAttr32
MAGIC = "**ACE**"
OS_MSDOS = 0
OS_WIN32 = 2
HOST_OS = {
0: "MS-DOS",
1: "OS/2",
2: "Win32",
3: "Unix",
4: "MAC-OS",
5: "Win NT",
6: "Primos",
7: "APPLE GS",
8: "ATARI",
9: "VAX VMS",
10: "AMIGA",
11: "NEXT",
}
COMPRESSION_TYPE = {
0: "Store",
1: "Lempel-Ziv 77",
2: "ACE v2.0",
}
COMPRESSION_MODE = {
0: "fastest",
1: "fast",
2: "normal",
3: "good",
4: "best",
}
# TODO: Computing the CRC16 would also prove useful
#def markerValidate(self):
# return not self["extend"].value and self["signature"].value == MAGIC and \
# self["host_os"].value<12
class MarkerFlags(StaticFieldSet):
format = (
(Bit, "extend", "Whether the header is extended"),
(Bit, "has_comment", "Whether the archive has a comment"),
(NullBits, "unused", 7, "Reserved bits"),
(Bit, "sfx", "SFX"),
(Bit, "limited_dict", "Junior SFX with 256K dictionary"),
(Bit, "multi_volume", "Part of a set of ACE archives"),
(Bit, "has_av_string", "This header holds an AV-string"),
(Bit, "recovery_record", "Recovery record preset"),
(Bit, "locked", "Archive is locked"),
(Bit, "solid", "Archive uses solid compression")
)
def markerFlags(self):
yield MarkerFlags(self, "flags", "Marker flags")
def markerHeader(self):
yield String(self, "signature", 7, "Signature")
yield UInt8(self, "ver_extract", "Version needed to extract archive")
yield UInt8(self, "ver_created", "Version used to create archive")
yield Enum(UInt8(self, "host_os", "OS where the files were compressed"), HOST_OS)
yield UInt8(self, "vol_num", "Volume number")
yield TimeDateMSDOS32(self, "time", "Date and time (MS DOS format)")
yield Bits(self, "reserved", 64, "Reserved size for future extensions")
flags = self["flags"]
if flags["has_av_string"].value:
yield PascalString8(self, "av_string", "AV String")
if flags["has_comment"].value:
size = filesizeHandler(UInt16(self, "comment_size", "Comment size"))
yield size
if size.value > 0:
yield RawBytes(self, "compressed_comment", size.value, \
"Compressed comment")
class FileFlags(StaticFieldSet):
format = (
(Bit, "extend", "Whether the header is extended"),
(Bit, "has_comment", "Presence of file comment"),
(Bits, "unused", 10, "Unused bit flags"),
(Bit, "encrypted", "File encrypted with password"),
(Bit, "previous", "File continued from previous volume"),
(Bit, "next", "File continues on the next volume"),
(Bit, "solid", "File compressed using previously archived files")
)
def fileFlags(self):
yield FileFlags(self, "flags", "File flags")
def fileHeader(self):
yield filesizeHandler(UInt32(self, "compressed_size", "Size of the compressed file"))
yield filesizeHandler(UInt32(self, "uncompressed_size", "Uncompressed file size"))
yield TimeDateMSDOS32(self, "ftime", "Date and time (MS DOS format)")
if self["/header/host_os"].value in (OS_MSDOS, OS_WIN32):
yield MSDOSFileAttr32(self, "file_attr", "File attributes")
else:
yield textHandler(UInt32(self, "file_attr", "File attributes"), hexadecimal)
yield textHandler(UInt32(self, "file_crc32", "CRC32 checksum over the compressed file)"), hexadecimal)
yield Enum(UInt8(self, "compression_type", "Type of compression"), COMPRESSION_TYPE)
yield Enum(UInt8(self, "compression_mode", "Quality of compression"), COMPRESSION_MODE)
yield textHandler(UInt16(self, "parameters", "Compression parameters"), hexadecimal)
yield textHandler(UInt16(self, "reserved", "Reserved data"), hexadecimal)
# Filename
yield PascalString16(self, "filename", "Filename")
# Comment
if self["flags/has_comment"].value:
yield filesizeHandler(UInt16(self, "comment_size", "Size of the compressed comment"))
if self["comment_size"].value > 0:
yield RawBytes(self, "comment_data", self["comment_size"].value, "Comment data")
def fileBody(self):
size = self["compressed_size"].value
if size > 0:
yield RawBytes(self, "compressed_data", size, "Compressed data")
def fileDesc(self):
return "File entry: %s (%s)" % (self["filename"].value, self["compressed_size"].display)
def recoveryHeader(self):
yield filesizeHandler(UInt32(self, "rec_blk_size", "Size of recovery data"))
self.body_size = self["rec_blk_size"].size
yield String(self, "signature", 7, "Signature, normally '**ACE**'")
yield textHandler(UInt32(self, "relative_start",
"Relative start (to this block) of the data this block is mode of"),
hexadecimal)
yield UInt32(self, "num_blocks", "Number of blocks the data is split into")
yield UInt32(self, "size_blocks", "Size of these blocks")
yield UInt16(self, "crc16_blocks", "CRC16 over recovery data")
# size_blocks blocks of size size_blocks follow
# The ultimate data is the xor data of all those blocks
size = self["size_blocks"].value
for index in xrange(self["num_blocks"].value):
yield RawBytes(self, "data[]", size, "Recovery block %i" % index)
yield RawBytes(self, "xor_data", size, "The XOR value of the above data blocks")
def recoveryDesc(self):
return "Recovery block, size=%u" % self["body_size"].display
def newRecoveryHeader(self):
"""
This header is described nowhere
"""
if self["flags/extend"].value:
yield filesizeHandler(UInt32(self, "body_size", "Size of the unknown body following"))
self.body_size = self["body_size"].value
yield textHandler(UInt32(self, "unknown[]", "Unknown field, probably 0"),
hexadecimal)
yield String(self, "signature", 7, "Signature, normally '**ACE**'")
yield textHandler(UInt32(self, "relative_start",
"Offset (=crc16's) of this block in the file"), hexadecimal)
yield textHandler(UInt32(self, "unknown[]",
"Unknown field, probably 0"), hexadecimal)
class BaseFlags(StaticFieldSet):
format = (
(Bit, "extend", "Whether the header is extended"),
(NullBits, "unused", 15, "Unused bit flags")
)
def parseFlags(self):
yield BaseFlags(self, "flags", "Unknown flags")
def parseHeader(self):
if self["flags/extend"].value:
yield filesizeHandler(UInt32(self, "body_size", "Size of the unknown body following"))
self.body_size = self["body_size"].value
def parseBody(self):
if self.body_size > 0:
yield RawBytes(self, "body_data", self.body_size, "Body data, unhandled")
class Block(FieldSet):
TAG_INFO = {
0: ("header", "Archiver header", markerFlags, markerHeader, None),
1: ("file[]", fileDesc, fileFlags, fileHeader, fileBody),
2: ("recovery[]", recoveryDesc, recoveryHeader, None, None),
5: ("new_recovery[]", None, None, newRecoveryHeader, None)
}
def __init__(self, parent, name, description=None):
FieldSet.__init__(self, parent, name, description)
self.body_size = 0
self.desc_func = None
type = self["block_type"].value
if type in self.TAG_INFO:
self._name, desc, self.parseFlags, self.parseHeader, self.parseBody = self.TAG_INFO[type]
if desc:
if isinstance(desc, str):
self._description = desc
else:
self.desc_func = desc
else:
self.warning("Processing as unknown block block of type %u" % type)
if not self.parseFlags:
self.parseFlags = parseFlags
if not self.parseHeader:
self.parseHeader = parseHeader
if not self.parseBody:
self.parseBody = parseBody
def createFields(self):
yield textHandler(UInt16(self, "crc16", "Archive CRC16 (from byte 4 on)"), hexadecimal)
yield filesizeHandler(UInt16(self, "head_size", "Block size (from byte 4 on)"))
yield UInt8(self, "block_type", "Block type")
# Flags
for flag in self.parseFlags(self):
yield flag
# Rest of the header
for field in self.parseHeader(self):
yield field
size = self["head_size"].value - (self.current_size//8) + (2+2)
if size > 0:
yield RawBytes(self, "extra_data", size, "Extra header data, unhandled")
# Body in itself
for field in self.parseBody(self):
yield field
def createDescription(self):
if self.desc_func:
return self.desc_func(self)
else:
return "Block: %s" % self["type"].display
class AceFile(Parser):
endian = LITTLE_ENDIAN
PARSER_TAGS = {
"id": "ace",
"category": "archive",
"file_ext": ("ace",),
"mime": (u"application/x-ace-compressed",),
"min_size": 50*8,
"description": "ACE archive"
}
def validate(self):
if self.stream.readBytes(7*8, len(MAGIC)) != MAGIC:
return "Invalid magic"
return True
def createFields(self):
while not self.eof:
yield Block(self, "block[]")
-52
View File
@@ -1,52 +0,0 @@
"""
GNU ar archive : archive file (.a) and Debian (.deb) archive.
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet, ParserError,
String, RawBytes, UnixLine)
from hachoir_core.endian import BIG_ENDIAN
class ArchiveFileEntry(FieldSet):
def createFields(self):
yield UnixLine(self, "header", "Header")
info = self["header"].value.split()
if len(info) != 7:
raise ParserError("Invalid file entry header")
size = int(info[5])
if 0 < size:
yield RawBytes(self, "content", size, "File data")
def createDescription(self):
return "File entry (%s)" % self["header"].value.split()[0]
class ArchiveFile(Parser):
endian = BIG_ENDIAN
MAGIC = '!<arch>\n'
PARSER_TAGS = {
"id": "unix_archive",
"category": "archive",
"file_ext": ("a", "deb"),
"mime":
(u"application/x-debian-package",
u"application/x-archive",
u"application/x-dpkg"),
"min_size": (8 + 13)*8, # file signature + smallest file as possible
"magic": ((MAGIC, 0),),
"description": "Unix archive"
}
def validate(self):
if self.stream.readBytes(0, len(self.MAGIC)) != self.MAGIC:
return "Invalid magic string"
return True
def createFields(self):
yield String(self, "id", 8, "Unix archive identifier (\"<!arch>\")", charset="ASCII")
while not self.eof:
data = self.stream.readBytes(self.current_size, 1)
if data == "\n":
yield RawBytes(self, "empty_line[]", 1, "Empty line")
else:
yield ArchiveFileEntry(self, "file[]", "File")
@@ -1,85 +0,0 @@
"""
BZIP2 archive file
Author: Victor Stinner
"""
from hachoir_parser import Parser
from hachoir_core.field import (ParserError, String,
Bytes, Character, UInt8, UInt32, CompressedField)
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_core.text_handler import textHandler, hexadecimal
try:
from bz2 import BZ2Decompressor
class Bunzip2:
def __init__(self, stream):
self.bzip2 = BZ2Decompressor()
def __call__(self, size, data=''):
try:
return self.bzip2.decompress(data)
except EOFError:
return ''
has_deflate = True
except ImportError:
has_deflate = False
class Bzip2Parser(Parser):
PARSER_TAGS = {
"id": "bzip2",
"category": "archive",
"file_ext": ("bz2",),
"mime": (u"application/x-bzip2",),
"min_size": 10*8,
"magic": (('BZh', 0),),
"description": "bzip2 archive"
}
endian = LITTLE_ENDIAN
def validate(self):
if self.stream.readBytes(0, 3) != 'BZh':
return "Wrong file signature"
if not("1" <= self["blocksize"].value <= "9"):
return "Wrong blocksize"
return True
def createFields(self):
yield String(self, "id", 3, "Identifier (BZh)", charset="ASCII")
yield Character(self, "blocksize", "Block size (KB of memory needed to uncompress)")
yield UInt8(self, "blockheader", "Block header")
if self["blockheader"].value == 0x17:
yield String(self, "id2", 4, "Identifier2 (re8P)", charset="ASCII")
yield UInt8(self, "id3", "Identifier3 (0x90)")
elif self["blockheader"].value == 0x31:
yield String(self, "id2", 5, "Identifier 2 (AY&SY)", charset="ASCII")
if self["id2"].value != "AY&SY":
raise ParserError("Invalid identifier 2 (AY&SY)!")
else:
raise ParserError("Invalid block header!")
yield textHandler(UInt32(self, "crc32", "CRC32"), hexadecimal)
if self._size is None: # TODO: is it possible to handle piped input?
raise NotImplementedError
size = (self._size - self.current_size)/8
if size:
for tag, filename in self.stream.tags:
if tag == "filename" and filename.endswith(".bz2"):
filename = filename[:-4]
break
else:
filename = None
data = Bytes(self, "file", size)
if has_deflate:
CompressedField(self, Bunzip2)
def createInputStream(**args):
if filename:
args.setdefault("tags",[]).append(("filename", filename))
return self._createInputStream(**args)
data._createInputStream = createInputStream
yield data
-125
View File
@@ -1,125 +0,0 @@
"""
Microsoft Cabinet (CAB) archive.
Author: Victor Stinner
Creation date: 31 january 2007
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet, Enum,
CString, String,
UInt16, UInt32, Bit, Bits, PaddingBits, NullBits,
DateTimeMSDOS32, RawBytes)
from hachoir_parser.common.msdos import MSDOSFileAttr16
from hachoir_core.text_handler import textHandler, hexadecimal, filesizeHandler
from hachoir_core.endian import LITTLE_ENDIAN
MAX_NB_FOLDER = 30
COMPRESSION_NONE = 0
COMPRESSION_NAME = {
0: "Uncompressed",
1: "Deflate",
2: "Quantum",
3: "LZX",
}
class Folder(FieldSet):
def createFields(self):
yield UInt32(self, "off_data", "Offset of data")
yield UInt16(self, "cf_data")
yield Enum(Bits(self, "compr_method", 4, "Compression method"), COMPRESSION_NAME)
yield Bits(self, "compr_level", 5, "Compression level")
yield PaddingBits(self, "padding", 7)
def createDescription(self):
text= "Folder: compression %s" % self["compr_method"].display
if self["compr_method"].value != COMPRESSION_NONE:
text += " (level %u)" % self["compr_level"].value
return text
class File(FieldSet):
def createFields(self):
yield filesizeHandler(UInt32(self, "filesize", "Uncompressed file size"))
yield UInt32(self, "offset", "File offset after decompression")
yield UInt16(self, "iFolder", "file control id")
yield DateTimeMSDOS32(self, "timestamp")
yield MSDOSFileAttr16(self, "attributes")
yield CString(self, "filename", charset="ASCII")
def createDescription(self):
return "File %s (%s)" % (
self["filename"].display, self["filesize"].display)
class Reserved(FieldSet):
def createFields(self):
yield UInt32(self, "size")
size = self["size"].value
if size:
yield RawBytes(self, "data", size)
class Flags(FieldSet):
static_size = 16
def createFields(self):
yield Bit(self, "has_previous")
yield Bit(self, "has_next")
yield Bit(self, "has_reserved")
yield NullBits(self, "padding", 13)
class CabFile(Parser):
endian = LITTLE_ENDIAN
MAGIC = "MSCF"
PARSER_TAGS = {
"id": "cab",
"category": "archive",
"file_ext": ("cab",),
"mime": (u"application/vnd.ms-cab-compressed",),
"magic": ((MAGIC, 0),),
"min_size": 1*8, # header + file entry
"description": "Microsoft Cabinet archive"
}
def validate(self):
if self.stream.readBytes(0, 4) != self.MAGIC:
return "Invalid magic"
if self["cab_version"].value != 0x0103:
return "Unknown version (%s)" % self["cab_version"].display
if not (1 <= self["nb_folder"].value <= MAX_NB_FOLDER):
return "Invalid number of folder (%s)" % self["nb_folder"].value
return True
def createFields(self):
yield String(self, "magic", 4, "Magic (MSCF)", charset="ASCII")
yield textHandler(UInt32(self, "hdr_checksum", "Header checksum (0 if not used)"), hexadecimal)
yield filesizeHandler(UInt32(self, "filesize", "Cabinet file size"))
yield textHandler(UInt32(self, "fld_checksum", "Folders checksum (0 if not used)"), hexadecimal)
yield UInt32(self, "off_file", "Offset of first file")
yield textHandler(UInt32(self, "files_checksum", "Files checksum (0 if not used)"), hexadecimal)
yield textHandler(UInt16(self, "cab_version", "Cabinet version"), hexadecimal)
yield UInt16(self, "nb_folder", "Number of folders")
yield UInt16(self, "nb_files", "Number of files")
yield Flags(self, "flags")
yield UInt16(self, "setid")
yield UInt16(self, "number", "Zero-based cabinet number")
# --- TODO: Support flags
if self["flags/has_reserved"].value:
yield Reserved(self, "reserved")
#(3) Previous cabinet name, if CAB_HEADER.flags & CAB_FLAG_HASPREV
#(4) Previous disk name, if CAB_HEADER.flags & CAB_FLAG_HASPREV
#(5) Next cabinet name, if CAB_HEADER.flags & CAB_FLAG_HASNEXT
#(6) Next disk name, if CAB_HEADER.flags & CAB_FLAG_HASNEXT
# ----
for index in xrange(self["nb_folder"].value):
yield Folder(self, "folder[]")
for index in xrange(self["nb_files"].value):
yield File(self, "file[]")
end = self.seekBit(self.size, "endraw")
if end:
yield end
def createContentSize(self):
return self["filesize"].value * 8
-129
View File
@@ -1,129 +0,0 @@
"""
GZIP archive parser.
Author: Victor Stinner
"""
from hachoir_parser import Parser
from hachoir_core.field import (
UInt8, UInt16, UInt32, Enum, TimestampUnix32,
Bit, CString, SubFile,
NullBits, Bytes, RawBytes)
from hachoir_core.text_handler import textHandler, hexadecimal, filesizeHandler
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_parser.common.deflate import Deflate
class GzipParser(Parser):
endian = LITTLE_ENDIAN
PARSER_TAGS = {
"id": "gzip",
"category": "archive",
"file_ext": ("gz",),
"mime": (u"application/x-gzip",),
"min_size": 18*8,
#"magic": (('\x1F\x8B\x08', 0),),
"magic_regex": (
# (magic, compression=deflate, <flags>, <mtime>, )
('\x1F\x8B\x08.{5}[\0\2\4\6][\x00-\x0D]', 0),
),
"description": u"gzip archive",
}
os_name = {
0: u"FAT filesystem",
1: u"Amiga",
2: u"VMS (or OpenVMS)",
3: u"Unix",
4: u"VM/CMS",
5: u"Atari TOS",
6: u"HPFS filesystem (OS/2, NT)",
7: u"Macintosh",
8: u"Z-System",
9: u"CP/M",
10: u"TOPS-20",
11: u"NTFS filesystem (NT)",
12: u"QDOS",
13: u"Acorn RISCOS",
}
COMPRESSION_NAME = {
8: u"deflate",
}
def validate(self):
if self["signature"].value != '\x1F\x8B':
return "Invalid signature"
if self["compression"].value not in self.COMPRESSION_NAME:
return "Unknown compression method (%u)" % self["compression"].value
if self["reserved[0]"].value != 0:
return "Invalid reserved[0] value"
if self["reserved[1]"].value != 0:
return "Invalid reserved[1] value"
if self["reserved[2]"].value != 0:
return "Invalid reserved[2] value"
return True
def createFields(self):
# Gzip header
yield Bytes(self, "signature", 2, r"GZip file signature (\x1F\x8B)")
yield Enum(UInt8(self, "compression", "Compression method"), self.COMPRESSION_NAME)
# Flags
yield Bit(self, "is_text", "File content is probably ASCII text")
yield Bit(self, "has_crc16", "Header CRC16")
yield Bit(self, "has_extra", "Extra informations (variable size)")
yield Bit(self, "has_filename", "Contains filename?")
yield Bit(self, "has_comment", "Contains comment?")
yield NullBits(self, "reserved[]", 3)
yield TimestampUnix32(self, "mtime", "Modification time")
# Extra flags
yield NullBits(self, "reserved[]", 1)
yield Bit(self, "slowest", "Compressor used maximum compression (slowest)")
yield Bit(self, "fastest", "Compressor used the fastest compression")
yield NullBits(self, "reserved[]", 5)
yield Enum(UInt8(self, "os", "Operating system"), self.os_name)
# Optional fields
if self["has_extra"].value:
yield UInt16(self, "extra_length", "Extra length")
yield RawBytes(self, "extra", self["extra_length"].value, "Extra")
if self["has_filename"].value:
yield CString(self, "filename", "Filename", charset="ISO-8859-1")
if self["has_comment"].value:
yield CString(self, "comment", "Comment")
if self["has_crc16"].value:
yield textHandler(UInt16(self, "hdr_crc16", "CRC16 of the header"),
hexadecimal)
if self._size is None: # TODO: is it possible to handle piped input?
raise NotImplementedError()
# Read file
size = (self._size - self.current_size) // 8 - 8 # -8: crc32+size
if 0 < size:
if self["has_filename"].value:
filename = self["filename"].value
else:
for tag, filename in self.stream.tags:
if tag == "filename" and filename.endswith(".gz"):
filename = filename[:-3]
break
else:
filename = None
yield Deflate(SubFile(self, "file", size, filename=filename))
# Footer
yield textHandler(UInt32(self, "crc32",
"Uncompressed data content CRC32"), hexadecimal)
yield filesizeHandler(UInt32(self, "size", "Uncompressed size"))
def createDescription(self):
desc = u"gzip archive"
info = []
if "filename" in self:
info.append('filename "%s"' % self["filename"].value)
if "size" in self:
info.append("was %s" % self["size"].display)
if self["mtime"].value:
info.append(self["mtime"].display)
return "%s: %s" % (desc, ", ".join(info))
-67
View File
@@ -1,67 +0,0 @@
"""
Microsoft Archive parser
Author: Victor Stinner
Creation date: 2007-03-04
"""
MAX_NB_FILE = 100000
from hachoir_parser import Parser
from hachoir_core.field import FieldSet, String, UInt32, SubFile
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_core.text_handler import textHandler, filesizeHandler, hexadecimal
class FileIndex(FieldSet):
static_size = 68*8
def createFields(self):
yield String(self, "filename", 56, truncate="\0", charset="ASCII")
yield filesizeHandler(UInt32(self, "filesize"))
yield textHandler(UInt32(self, "crc32"), hexadecimal)
yield UInt32(self, "offset")
def createDescription(self):
return "File %s (%s) at %s" % (
self["filename"].value, self["filesize"].display, self["offset"].value)
class MarFile(Parser):
MAGIC = "MARC"
PARSER_TAGS = {
"id": "mar",
"category": "archive",
"file_ext": ("mar",),
"min_size": 80*8, # At least one file index
"magic": ((MAGIC, 0),),
"description": "Microsoft Archive",
}
endian = LITTLE_ENDIAN
def validate(self):
if self.stream.readBytes(0, 4) != self.MAGIC:
return "Invalid magic"
if self["version"].value != 3:
return "Invalid version"
if not(1 <= self["nb_file"].value <= MAX_NB_FILE):
return "Invalid number of file"
return True
def createFields(self):
yield String(self, "magic", 4, "File signature (MARC)", charset="ASCII")
yield UInt32(self, "version")
yield UInt32(self, "nb_file")
files = []
for index in xrange(self["nb_file"].value):
item = FileIndex(self, "file[]")
yield item
if item["filesize"].value:
files.append(item)
files.sort(key=lambda item: item["offset"].value)
for index in files:
padding = self.seekByte(index["offset"].value)
if padding:
yield padding
size = index["filesize"].value
desc = "File %s" % index["filename"].value
yield SubFile(self, "data[]", size, desc, filename=index["filename"].value)
-353
View File
@@ -1,353 +0,0 @@
"""
RAR parser
Status: can only read higher-level attructures
Author: Christophe Gisquet
"""
from hachoir_parser import Parser
from hachoir_core.field import (StaticFieldSet, FieldSet,
Bit, Bits, Enum,
UInt8, UInt16, UInt32, UInt64,
String, TimeDateMSDOS32,
NullBytes, NullBits, RawBytes)
from hachoir_core.text_handler import textHandler, filesizeHandler, hexadecimal
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_parser.common.msdos import MSDOSFileAttr32
MAX_FILESIZE = 1000 * 1024 * 1024
BLOCK_NAME = {
0x72: "Marker",
0x73: "Archive",
0x74: "File",
0x75: "Comment",
0x76: "Extra info",
0x77: "Subblock",
0x78: "Recovery record",
0x79: "Archive authenticity",
0x7A: "New-format subblock",
0x7B: "Archive end",
}
COMPRESSION_NAME = {
0x30: "Storing",
0x31: "Fastest compression",
0x32: "Fast compression",
0x33: "Normal compression",
0x34: "Good compression",
0x35: "Best compression"
}
OS_MSDOS = 0
OS_WIN32 = 2
OS_NAME = {
0: "MS DOS",
1: "OS/2",
2: "Win32",
3: "Unix",
}
DICTIONARY_SIZE = {
0: "Dictionary size 64 Kb",
1: "Dictionary size 128 Kb",
2: "Dictionary size 256 Kb",
3: "Dictionary size 512 Kb",
4: "Dictionary size 1024 Kb",
7: "File is a directory",
}
def formatRARVersion(field):
"""
Decodes the RAR version stored on 1 byte
"""
return "%u.%u" % divmod(field.value, 10)
def commonFlags(s):
yield Bit(s, "has_added_size", "Additional field indicating additional size")
yield Bit(s, "is_ignorable", "Old versions of RAR should ignore this block when copying data")
class ArchiveFlags(StaticFieldSet):
format = (
(Bit, "vol", "Archive volume"),
(Bit, "has_comment", "Whether there is a comment"),
(Bit, "is_locked", "Archive volume"),
(Bit, "is_solid", "Whether files can be extracted separately"),
(Bit, "new_numbering", "New numbering, or compressed comment"), # From unrar
(Bit, "has_authenticity_information", "The integrity/authenticity of the archive can be checked"),
(Bit, "is_protected", "The integrity/authenticity of the archive can be checked"),
(Bit, "is_passworded", "Needs a password to be decrypted"),
(Bit, "is_first_vol", "Whether it is the first volume"),
(Bit, "is_encrypted", "Whether the encryption version is present"),
(NullBits, "internal", 6, "Reserved for 'internal use'")
)
def archiveFlags(s):
yield ArchiveFlags(s, "flags", "Archiver block flags")
def archiveHeader(s):
yield NullBytes(s, "reserved[]", 2, "Reserved word")
yield NullBytes(s, "reserved[]", 4, "Reserved dword")
def commentHeader(s):
yield filesizeHandler(UInt16(s, "total_size", "Comment header size + comment size"))
yield filesizeHandler(UInt16(s, "uncompressed_size", "Uncompressed comment size"))
yield UInt8(s, "required_version", "RAR version needed to extract comment")
yield UInt8(s, "packing_method", "Comment packing method")
yield UInt16(s, "comment_crc16", "Comment CRC")
def commentBody(s):
size = s["total_size"].value - s.current_size
if size > 0:
yield RawBytes(s, "comment_data", size, "Compressed comment data")
def signatureHeader(s):
yield TimeDateMSDOS32(s, "creation_time")
yield filesizeHandler(UInt16(s, "arc_name_size"))
yield filesizeHandler(UInt16(s, "user_name_size"))
def recoveryHeader(s):
yield filesizeHandler(UInt32(s, "total_size"))
yield textHandler(UInt8(s, "version"), hexadecimal)
yield UInt16(s, "rec_sectors")
yield UInt32(s, "total_blocks")
yield RawBytes(s, "mark", 8)
def avInfoHeader(s):
yield filesizeHandler(UInt16(s, "total_size", "Total block size"))
yield UInt8(s, "version", "Version needed to decompress", handler=hexadecimal)
yield UInt8(s, "method", "Compression method", handler=hexadecimal)
yield UInt8(s, "av_version", "Version for AV", handler=hexadecimal)
yield UInt32(s, "av_crc", "AV info CRC32", handler=hexadecimal)
def avInfoBody(s):
size = s["total_size"].value - s.current_size
if size > 0:
yield RawBytes(s, "av_info_data", size, "AV info")
class FileFlags(FieldSet):
static_size = 16
def createFields(self):
yield Bit(self, "continued_from", "File continued from previous volume")
yield Bit(self, "continued_in", "File continued in next volume")
yield Bit(self, "is_encrypted", "File encrypted with password")
yield Bit(self, "has_comment", "File comment present")
yield Bit(self, "is_solid", "Information from previous files is used (solid flag)")
# The 3 following lines are what blocks more staticity
yield Enum(Bits(self, "dictionary_size", 3, "Dictionary size"), DICTIONARY_SIZE)
for bit in commonFlags(self):
yield bit
yield Bit(self, "is_large", "file64 operations needed")
yield Bit(self, "is_unicode", "Filename also encoded using Unicode")
yield Bit(self, "has_salt", "Has salt for encryption")
yield Bit(self, "uses_file_version", "File versioning is used")
yield Bit(self, "has_ext_time", "Extra time ??")
yield Bit(self, "has_ext_flags", "Extra flag ??")
def fileFlags(s):
yield FileFlags(s, "flags", "File block flags")
class ExtTime(FieldSet):
def createFields(self):
yield textHandler(UInt16(self, "time_flags", "Flags for extended time"), hexadecimal)
flags = self["time_flags"].value
for index in xrange(4):
rmode = flags >> ((3-index)*4)
if rmode & 8:
if index:
yield TimeDateMSDOS32(self, "dos_time[]", "DOS Time")
if rmode & 3:
yield RawBytes(self, "remainder[]", rmode & 3, "Time remainder")
def specialHeader(s, is_file):
yield filesizeHandler(UInt32(s, "compressed_size", "Compressed size (bytes)"))
yield filesizeHandler(UInt32(s, "uncompressed_size", "Uncompressed size (bytes)"))
yield Enum(UInt8(s, "host_os", "Operating system used for archiving"), OS_NAME)
yield textHandler(UInt32(s, "crc32", "File CRC32"), hexadecimal)
yield TimeDateMSDOS32(s, "ftime", "Date and time (MS DOS format)")
yield textHandler(UInt8(s, "version", "RAR version needed to extract file"), formatRARVersion)
yield Enum(UInt8(s, "method", "Packing method"), COMPRESSION_NAME)
yield filesizeHandler(UInt16(s, "filename_length", "File name size"))
if s["host_os"].value in (OS_MSDOS, OS_WIN32):
yield MSDOSFileAttr32(s, "file_attr", "File attributes")
else:
yield textHandler(UInt32(s, "file_attr", "File attributes"), hexadecimal)
# Start additional field from unrar
if s["flags/is_large"].value:
yield filesizeHandler(UInt64(s, "large_size", "Extended 64bits filesize"))
# End additional field
size = s["filename_length"].value
if size > 0:
if s["flags/is_unicode"].value:
charset = "UTF-8"
else:
charset = "ISO-8859-15"
yield String(s, "filename", size, "Filename", charset=charset)
# Start additional fields from unrar - file only
if is_file:
if s["flags/has_salt"].value:
yield textHandler(UInt8(s, "salt", "Salt"), hexadecimal)
if s["flags/has_ext_time"].value:
yield ExtTime(s, "extra_time", "Extra time info")
def fileHeader(s):
return specialHeader(s, True)
def fileBody(s):
# File compressed data
size = s["compressed_size"].value
if s["flags/is_large"].value:
size += s["large_size"].value
if size > 0:
yield RawBytes(s, "compressed_data", size, "File compressed data")
def fileDescription(s):
return "File entry: %s (%s)" % \
(s["filename"].display, s["compressed_size"].display)
def newSubHeader(s):
return specialHeader(s, False)
class EndFlags(StaticFieldSet):
format = (
(Bit, "has_next_vol", "Whether there is another next volume"),
(Bit, "has_data_crc", "Whether a CRC value is present"),
(Bit, "rev_space"),
(Bit, "has_vol_number", "Whether the volume number is present"),
(Bits, "unused[]", 4),
(Bit, "has_added_size", "Additional field indicating additional size"),
(Bit, "is_ignorable", "Old versions of RAR should ignore this block when copying data"),
(Bits, "unused[]", 6),
)
def endFlags(s):
yield EndFlags(s, "flags", "End block flags")
class BlockFlags(FieldSet):
static_size = 16
def createFields(self):
yield textHandler(Bits(self, "unused[]", 8, "Unused flag bits"), hexadecimal)
yield Bit(self, "has_added_size", "Additional field indicating additional size")
yield Bit(self, "is_ignorable", "Old versions of RAR should ignore this block when copying data")
yield Bits(self, "unused[]", 6)
class Block(FieldSet):
BLOCK_INFO = {
# None means 'use default function'
0x72: ("marker", "Archive header", None, None, None),
0x73: ("archive_start", "Archive info", archiveFlags, archiveHeader, None),
0x74: ("file[]", fileDescription, fileFlags, fileHeader, fileBody),
0x75: ("comment[]", "Stray comment", None, commentHeader, commentBody),
0x76: ("av_info[]", "Extra information", None, avInfoHeader, avInfoBody),
0x77: ("sub_block[]", "Stray subblock", None, newSubHeader, fileBody),
0x78: ("recovery[]", "Recovery block", None, recoveryHeader, None),
0x79: ("signature", "Signature block", None, signatureHeader, None),
0x7A: ("new_sub_block[]", "Stray new-format subblock", fileFlags,
newSubHeader, fileBody),
0x7B: ("archive_end", "Archive end block", endFlags, None, None),
}
def __init__(self, parent, name):
FieldSet.__init__(self, parent, name)
t = self["block_type"].value
if t in self.BLOCK_INFO:
self._name, desc, parseFlags, parseHeader, parseBody = self.BLOCK_INFO[t]
if callable(desc):
self.createDescription = lambda: desc(self)
elif desc:
self._description = desc
if parseFlags : self.parseFlags = lambda: parseFlags(self)
if parseHeader : self.parseHeader = lambda: parseHeader(self)
if parseBody : self.parseBody = lambda: parseBody(self)
else:
self.info("Processing as unknown block block of type %u" % type)
self._size = 8*self["block_size"].value
if t == 0x74 or t == 0x7A:
self._size += 8*self["compressed_size"].value
if "is_large" in self["flags"] and self["flags/is_large"].value:
self._size += 8*self["large_size"].value
elif "has_added_size" in self:
self._size += 8*self["added_size"].value
# TODO: check if any other member is needed here
def createFields(self):
yield textHandler(UInt16(self, "crc16", "Block CRC16"), hexadecimal)
yield textHandler(UInt8(self, "block_type", "Block type"), hexadecimal)
# Parse flags
for field in self.parseFlags():
yield field
# Get block size
yield filesizeHandler(UInt16(self, "block_size", "Block size"))
# Parse remaining header
for field in self.parseHeader():
yield field
# Finish header with stuff of unknow size
size = self["block_size"].value - (self.current_size//8)
if size > 0:
yield RawBytes(self, "unknown", size, "Unknow data (UInt32 probably)")
# Parse body
for field in self.parseBody():
yield field
def createDescription(self):
return "Block entry: %s" % self["type"].display
def parseFlags(self):
yield BlockFlags(self, "flags", "Block header flags")
def parseHeader(self):
if "has_added_size" in self["flags"] and \
self["flags/has_added_size"].value:
yield filesizeHandler(UInt32(self, "added_size",
"Supplementary block size"))
def parseBody(self):
"""
Parse what is left of the block
"""
size = self["block_size"].value - (self.current_size//8)
if "has_added_size" in self["flags"] and self["flags/has_added_size"].value:
size += self["added_size"].value
if size > 0:
yield RawBytes(self, "body", size, "Body data")
class RarFile(Parser):
MAGIC = "Rar!\x1A\x07\x00"
PARSER_TAGS = {
"id": "rar",
"category": "archive",
"file_ext": ("rar",),
"mime": (u"application/x-rar-compressed", ),
"min_size": 7*8,
"magic": ((MAGIC, 0),),
"description": "Roshal archive (RAR)",
}
endian = LITTLE_ENDIAN
def validate(self):
magic = self.MAGIC
if self.stream.readBytes(0, len(magic)) != magic:
return "Invalid magic"
return True
def createFields(self):
while not self.eof:
yield Block(self, "block[]")
def createContentSize(self):
start = 0
end = MAX_FILESIZE * 8
pos = self.stream.searchBytes("\xC4\x3D\x7B\x00\x40\x07\x00", start, end)
if pos is not None:
return pos + 7*8
return None
-267
View File
@@ -1,267 +0,0 @@
"""
RPM archive parser.
Author: Victor Stinner, 1st December 2005.
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet, ParserError,
UInt8, UInt16, UInt32, UInt64, Enum,
NullBytes, Bytes, RawBytes, SubFile,
Character, CString, String)
from hachoir_core.endian import BIG_ENDIAN
from hachoir_parser.archive.gzip_parser import GzipParser
from hachoir_parser.archive.bzip2_parser import Bzip2Parser
class ItemContent(FieldSet):
format_type = {
0: UInt8,
1: Character,
2: UInt8,
3: UInt16,
4: UInt32,
5: UInt64,
6: CString,
7: RawBytes,
8: CString,
9: CString
}
def __init__(self, parent, name, item):
FieldSet.__init__(self, parent, name, item.description)
self.related_item = item
self._name = "content_%s" % item.name
def createFields(self):
item = self.related_item
type = item["type"].value
cls = self.format_type[type]
count = item["count"].value
if cls is RawBytes: # or type == 8:
if cls is RawBytes:
args = (self, "value", count)
else:
args = (self, "value") # cls is CString
count = 1
else:
if 1 < count:
args = (self, "value[]")
else:
args = (self, "value")
for index in xrange(count):
yield cls(*args)
class Item(FieldSet):
type_name = {
0: "NULL",
1: "CHAR",
2: "INT8",
3: "INT16",
4: "INT32",
5: "INT64",
6: "CSTRING",
7: "BIN",
8: "CSTRING_ARRAY",
9: "CSTRING?"
}
tag_name = {
1000: "File size",
1001: "(Broken) MD5 signature",
1002: "PGP 2.6.3 signature",
1003: "(Broken) MD5 signature",
1004: "MD5 signature",
1005: "GnuPG signature",
1006: "PGP5 signature",
1007: "Uncompressed payload size (bytes)",
256+8: "Broken SHA1 header digest",
256+9: "Broken SHA1 header digest",
256+13: "Broken SHA1 header digest",
256+11: "DSA header signature",
256+12: "RSA header signature"
}
def __init__(self, parent, name, description=None, tag_name_dict=None):
FieldSet.__init__(self, parent, name, description)
if tag_name_dict is None:
tag_name_dict = Item.tag_name
self.tag_name_dict = tag_name_dict
def createFields(self):
yield Enum(UInt32(self, "tag", "Tag"), self.tag_name_dict)
yield Enum(UInt32(self, "type", "Type"), Item.type_name)
yield UInt32(self, "offset", "Offset")
yield UInt32(self, "count", "Count")
def createDescription(self):
return "Item: %s (%s)" % (self["tag"].display, self["type"].display)
class ItemHeader(Item):
tag_name = {
61: "Current image",
62: "Signatures",
63: "Immutable",
64: "Regions",
100: "I18N string locales",
1000: "Name",
1001: "Version",
1002: "Release",
1003: "Epoch",
1004: "Summary",
1005: "Description",
1006: "Build time",
1007: "Build host",
1008: "Install time",
1009: "Size",
1010: "Distribution",
1011: "Vendor",
1012: "Gif",
1013: "Xpm",
1014: "Licence",
1015: "Packager",
1016: "Group",
1017: "Changelog",
1018: "Source",
1019: "Patch",
1020: "Url",
1021: "OS",
1022: "Arch",
1023: "Prein",
1024: "Postin",
1025: "Preun",
1026: "Postun",
1027: "Old filenames",
1028: "File sizes",
1029: "File states",
1030: "File modes",
1031: "File uids",
1032: "File gids",
1033: "File rdevs",
1034: "File mtimes",
1035: "File MD5s",
1036: "File link to's",
1037: "File flags",
1038: "Root",
1039: "File username",
1040: "File groupname",
1043: "Icon",
1044: "Source rpm",
1045: "File verify flags",
1046: "Archive size",
1047: "Provide name",
1048: "Require flags",
1049: "Require name",
1050: "Require version",
1051: "No source",
1052: "No patch",
1053: "Conflict flags",
1054: "Conflict name",
1055: "Conflict version",
1056: "Default prefix",
1057: "Build root",
1058: "Install prefix",
1059: "Exclude arch",
1060: "Exclude OS",
1061: "Exclusive arch",
1062: "Exclusive OS",
1064: "RPM version",
1065: "Trigger scripts",
1066: "Trigger name",
1067: "Trigger version",
1068: "Trigger flags",
1069: "Trigger index",
1079: "Verify script",
#TODO: Finish the list (id 1070..1162 using rpm library source code)
}
def __init__(self, parent, name, description=None):
Item.__init__(self, parent, name, description, self.tag_name)
def sortRpmItem(a,b):
return int( a["offset"].value - b["offset"].value )
class PropertySet(FieldSet):
def __init__(self, parent, name, *args):
FieldSet.__init__(self, parent, name, *args)
self._size = self["content_item[1]"].address + self["size"].value * 8
def createFields(self):
# Read chunk header
yield Bytes(self, "signature", 3, r"Property signature (\x8E\xAD\xE8)")
if self["signature"].value != "\x8E\xAD\xE8":
raise ParserError("Invalid property signature")
yield UInt8(self, "version", "Signature version")
yield NullBytes(self, "reserved", 4, "Reserved")
yield UInt32(self, "count", "Count")
yield UInt32(self, "size", "Size")
# Read item header
items = []
for i in range(0, self["count"].value):
item = ItemHeader(self, "item[]")
yield item
items.append(item)
# Sort items by their offset
items.sort( sortRpmItem )
# Read item content
start = self.current_size/8
for item in items:
offset = item["offset"].value
diff = offset - (self.current_size/8 - start)
if 0 < diff:
yield NullBytes(self, "padding[]", diff)
yield ItemContent(self, "content[]", item)
size = start + self["size"].value - self.current_size/8
if 0 < size:
yield NullBytes(self, "padding[]", size)
class RpmFile(Parser):
PARSER_TAGS = {
"id": "rpm",
"category": "archive",
"file_ext": ("rpm",),
"mime": (u"application/x-rpm",),
"min_size": (96 + 16 + 16)*8, # file header + checksum + content header
"magic": (('\xED\xAB\xEE\xDB', 0),),
"description": "RPM package"
}
TYPE_NAME = {
0: "Binary",
1: "Source"
}
endian = BIG_ENDIAN
def validate(self):
if self["signature"].value != '\xED\xAB\xEE\xDB':
return "Invalid signature"
if self["major_ver"].value != 3:
return "Unknown major version (%u)" % self["major_ver"].value
if self["type"].value not in self.TYPE_NAME:
return "Invalid RPM type"
return True
def createFields(self):
yield Bytes(self, "signature", 4, r"RPM file signature (\xED\xAB\xEE\xDB)")
yield UInt8(self, "major_ver", "Major version")
yield UInt8(self, "minor_ver", "Minor version")
yield Enum(UInt16(self, "type", "RPM type"), RpmFile.TYPE_NAME)
yield UInt16(self, "architecture", "Architecture")
yield String(self, "name", 66, "Archive name", strip="\0", charset="ASCII")
yield UInt16(self, "os", "OS")
yield UInt16(self, "signature_type", "Type of signature")
yield NullBytes(self, "reserved", 16, "Reserved")
yield PropertySet(self, "checksum", "Checksum (signature)")
yield PropertySet(self, "header", "Header")
if self._size is None: # TODO: is it possible to handle piped input?
raise NotImplementedError
size = (self._size - self.current_size) // 8
if size:
if 3 <= size and self.stream.readBytes(self.current_size, 3) == "BZh":
yield SubFile(self, "content", size, "bzip2 content", parser=Bzip2Parser)
else:
yield SubFile(self, "content", size, "gzip content", parser=GzipParser)
-401
View File
@@ -1,401 +0,0 @@
"""
7zip file parser
Informations:
- File 7zformat.txt of 7-zip SDK:
http://www.7-zip.org/sdk.html
Author: Olivier SCHWAB
Creation date: 6 december 2006
"""
from hachoir_parser import Parser
from hachoir_core.field import (Field, FieldSet, ParserError,
GenericVector,
Enum, UInt8, UInt32, UInt64,
Bytes, RawBytes)
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_core.text_handler import textHandler, hexadecimal, filesizeHandler
class SZUInt64(Field):
"""
Variable length UInt64, where the first byte gives both the number of bytes
needed and the upper byte value.
"""
def __init__(self, parent, name, max_size=None, description=None):
Field.__init__(self, parent, name, size=8, description=description)
value = 0
addr = self.absolute_address
mask = 0x80
firstByte = parent.stream.readBits(addr, 8, LITTLE_ENDIAN)
for i in xrange(8):
addr += 8
if not (firstByte & mask):
value += ((firstByte & (mask-1)) << (8*i))
break
value |= (parent.stream.readBits(addr, 8, LITTLE_ENDIAN) << (8*i))
mask >>= 1
self._size += 8
self.createValue = lambda: value
ID_END, ID_HEADER, ID_ARCHIVE_PROPS, ID_ADD_STREAM_INFO, ID_MAIN_STREAM_INFO, \
ID_FILES_INFO, ID_PACK_INFO, ID_UNPACK_INFO, ID_SUBSTREAMS_INFO, ID_SIZE, \
ID_CRC, ID_FOLDER, ID_CODERS_UNPACK_SIZE, ID_NUM_UNPACK_STREAMS, \
ID_EMPTY_STREAM, ID_EMPTY_FILE, ID_ANTI, ID_NAME, ID_CREATION_TIME, \
ID_LAST_ACCESS_TIME, ID_LAST_WRITE_TIME, ID_WIN_ATTR, ID_COMMENT, \
ID_ENCODED_HEADER = xrange(24)
ID_INFO = {
ID_END : "End",
ID_HEADER : "Header embedding another one",
ID_ARCHIVE_PROPS : "Archive Properties",
ID_ADD_STREAM_INFO : "Additional Streams Info",
ID_MAIN_STREAM_INFO : "Main Streams Info",
ID_FILES_INFO : "Files Info",
ID_PACK_INFO : "Pack Info",
ID_UNPACK_INFO : "Unpack Info",
ID_SUBSTREAMS_INFO : "Substreams Info",
ID_SIZE : "Size",
ID_CRC : "CRC",
ID_FOLDER : "Folder",
ID_CODERS_UNPACK_SIZE: "Coders Unpacked size",
ID_NUM_UNPACK_STREAMS: "Number of Unpacked Streams",
ID_EMPTY_STREAM : "Empty Stream",
ID_EMPTY_FILE : "Empty File",
ID_ANTI : "Anti",
ID_NAME : "Name",
ID_CREATION_TIME : "Creation Time",
ID_LAST_ACCESS_TIME : "Last Access Time",
ID_LAST_WRITE_TIME : "Last Write Time",
ID_WIN_ATTR : "Win Attributes",
ID_COMMENT : "Comment",
ID_ENCODED_HEADER : "Header holding encoded data info",
}
class SkippedData(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "id[]"), ID_INFO)
size = SZUInt64(self, "size")
yield size
if size.value > 0:
yield RawBytes(self, "data", size.value)
def waitForID(s, wait_id, wait_name="waited_id[]"):
while not s.eof:
addr = s.absolute_address+s.current_size
uid = s.stream.readBits(addr, 8, LITTLE_ENDIAN)
if uid == wait_id:
yield Enum(UInt8(s, wait_name), ID_INFO)
s.info("Found ID %s (%u)" % (ID_INFO[uid], uid))
return
s.info("Skipping ID %u!=%u" % (uid, wait_id))
yield SkippedData(s, "skipped_id[]", "%u != %u" % (uid, wait_id))
class HashDigest(FieldSet):
def __init__(self, parent, name, num_digests, desc=None):
FieldSet.__init__(self, parent, name, desc)
self.num_digests = num_digests
def createFields(self):
yield Enum(UInt8(self, "id"), ID_INFO)
bytes = self.stream.readBytes(self.absolute_address, self.num_digests)
if self.num_digests > 0:
yield GenericVector(self, "defined[]", self.num_digests, UInt8, "bool")
for index in xrange(self.num_digests):
if bytes[index]:
yield textHandler(UInt32(self, "hash[]",
"Hash for digest %u" % index), hexadecimal)
class PackInfo(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "id"), ID_INFO)
# Very important, helps determine where the data is
yield SZUInt64(self, "pack_pos", "Position of the packs")
num = SZUInt64(self, "num_pack_streams")
yield num
num = num.value
for field in waitForID(self, ID_SIZE, "size_marker"):
yield field
for size in xrange(num):
yield SZUInt64(self, "pack_size[]")
while not self.eof:
addr = self.absolute_address+self.current_size
uid = self.stream.readBits(addr, 8, LITTLE_ENDIAN)
if uid == ID_END:
yield Enum(UInt8(self, "end_marker"), ID_INFO)
break
elif uid == ID_CRC:
yield HashDigest(self, "hash_digest", size)
else:
yield SkippedData(self, "skipped_data")
def lzmaParams(value):
param = value.value
remainder = param / 9
# Literal coder context bits
lc = param % 9
# Position state bits
pb = remainder / 5
# Literal coder position bits
lp = remainder % 5
return "lc=%u pb=%u lp=%u" % (lc, lp, pb)
class CoderID(FieldSet):
CODECS = {
# Only 2 methods ... and what about PPMD ?
"\0" : "copy",
"\3\1\1": "lzma",
}
def createFields(self):
byte = UInt8(self, "id_size")
yield byte
byte = byte.value
self.info("ID=%u" % byte)
size = byte & 0xF
if size > 0:
name = self.stream.readBytes(self.absolute_address+self.current_size, size)
if name in self.CODECS:
name = self.CODECS[name]
self.info("Codec is %s" % name)
else:
self.info("Undetermined codec %s" % name)
name = "unknown"
yield RawBytes(self, name, size)
#yield textHandler(Bytes(self, "id", size), lambda: name)
if byte & 0x10:
yield SZUInt64(self, "num_stream_in")
yield SZUInt64(self, "num_stream_out")
self.info("Streams: IN=%u OUT=%u" % \
(self["num_stream_in"].value, self["num_stream_out"].value))
if byte & 0x20:
size = SZUInt64(self, "properties_size[]")
yield size
if size.value == 5:
#LzmaDecodeProperties@LZMAStateDecode.c
yield textHandler(UInt8(self, "parameters"), lzmaParams)
yield filesizeHandler(UInt32(self, "dictionary_size"))
elif size.value > 0:
yield RawBytes(self, "properties[]", size.value)
class CoderInfo(FieldSet):
def __init__(self, parent, name, desc=None):
FieldSet.__init__(self, parent, name, desc)
self.in_streams = 1
self.out_streams = 1
def createFields(self):
# The real ID
addr = self.absolute_address + self.current_size
b = self.parent.stream.readBits(addr, 8, LITTLE_ENDIAN)
cid = CoderID(self, "coder_id")
yield cid
if b&0x10: # Work repeated, ...
self.in_streams = cid["num_stream_in"].value
self.out_streams = cid["num_stream_out"].value
# Skip other IDs
while b&0x80:
addr = self.absolute_address + self.current_size
b = self.parent.stream.readBits(addr, 8, LITTLE_ENDIAN)
yield CoderID(self, "unused_codec_id[]")
class BindPairInfo(FieldSet):
def createFields(self):
# 64 bits values then cast to 32 in fact
yield SZUInt64(self, "in_index")
yield SZUInt64(self, "out_index")
self.info("Indexes: IN=%u OUT=%u" % \
(self["in_index"].value, self["out_index"].value))
class FolderItem(FieldSet):
def __init__(self, parent, name, desc=None):
FieldSet.__init__(self, parent, name, desc)
self.in_streams = 0
self.out_streams = 0
def createFields(self):
yield SZUInt64(self, "num_coders")
num = self["num_coders"].value
self.info("Folder: %u codecs" % num)
# Coders info
for index in xrange(num):
ci = CoderInfo(self, "coder_info[]")
yield ci
self.in_streams += ci.in_streams
self.out_streams += ci.out_streams
# Bin pairs
self.info("out streams: %u" % self.out_streams)
for index in xrange(self.out_streams-1):
yield BindPairInfo(self, "bind_pair[]")
# Packed streams
# @todo: Actually find mapping
packed_streams = self.in_streams - self.out_streams + 1
if packed_streams == 1:
pass
else:
for index in xrange(packed_streams):
yield SZUInt64(self, "pack_stream[]")
class UnpackInfo(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "id"), ID_INFO)
# Wait for synch
for field in waitForID(self, ID_FOLDER, "folder_marker"):
yield field
yield SZUInt64(self, "num_folders")
# Get generic info
num = self["num_folders"].value
self.info("%u folders" % num)
yield UInt8(self, "is_external")
# Read folder items
for folder_index in xrange(num):
yield FolderItem(self, "folder_item[]")
# Get unpack sizes for each coder of each folder
for field in waitForID(self, ID_CODERS_UNPACK_SIZE, "coders_unpsize_marker"):
yield field
for folder_index in xrange(num):
folder_item = self["folder_item[%u]" % folder_index]
for index in xrange(folder_item.out_streams):
#yield UInt8(self, "unpack_size[]")
yield SZUInt64(self, "unpack_size[]")
# Extract digests
while not self.eof:
addr = self.absolute_address+self.current_size
uid = self.stream.readBits(addr, 8, LITTLE_ENDIAN)
if uid == ID_END:
yield Enum(UInt8(self, "end_marker"), ID_INFO)
break
elif uid == ID_CRC:
yield HashDigest(self, "hash_digest", num)
else:
yield SkippedData(self, "skip_data")
class SubStreamInfo(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "id"), ID_INFO)
raise ParserError("SubStreamInfo not implemented yet")
class EncodedHeader(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "id"), ID_INFO)
while not self.eof:
addr = self.absolute_address+self.current_size
uid = self.stream.readBits(addr, 8, LITTLE_ENDIAN)
if uid == ID_END:
yield Enum(UInt8(self, "end_marker"), ID_INFO)
break
elif uid == ID_PACK_INFO:
yield PackInfo(self, "pack_info", ID_INFO[ID_PACK_INFO])
elif uid == ID_UNPACK_INFO:
yield UnpackInfo(self, "unpack_info", ID_INFO[ID_UNPACK_INFO])
elif uid == ID_SUBSTREAMS_INFO:
yield SubStreamInfo(self, "substreams_info", ID_INFO[ID_SUBSTREAMS_INFO])
else:
self.info("Unexpected ID (%i)" % uid)
break
class IDHeader(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "id"), ID_INFO)
ParserError("IDHeader not implemented")
class NextHeader(FieldSet):
def __init__(self, parent, name, desc="Next header"):
FieldSet.__init__(self, parent, name, desc)
self._size = 8*self["/signature/start_hdr/next_hdr_size"].value
# Less work, as much interpretable information as the other
# version... what an obnoxious format
def createFields2(self):
yield Enum(UInt8(self, "header_type"), ID_INFO)
yield RawBytes(self, "header_data", self._size-1)
def createFields(self):
uid = self.stream.readBits(self.absolute_address, 8, LITTLE_ENDIAN)
if uid == ID_HEADER:
yield IDHeader(self, "header", ID_INFO[ID_HEADER])
elif uid == ID_ENCODED_HEADER:
yield EncodedHeader(self, "encoded_hdr", ID_INFO[ID_ENCODED_HEADER])
# Game Over: this is usually encoded using LZMA, not copy
# See SzReadAndDecodePackedStreams/SzDecode being called with the
# data position from "/next_hdr/encoded_hdr/pack_info/pack_pos"
# We should process further, yet we can't...
else:
ParserError("Unexpected ID %u" % uid)
size = self._size - self.current_size
if size > 0:
yield RawBytes(self, "next_hdr_data", size//8, "Next header's data")
class Body(FieldSet):
def __init__(self, parent, name, desc="Body data"):
FieldSet.__init__(self, parent, name, desc)
self._size = 8*self["/signature/start_hdr/next_hdr_offset"].value
def createFields(self):
if "encoded_hdr" in self["/next_hdr/"]:
pack_size = sum([s.value for s in self.array("/next_hdr/encoded_hdr/pack_info/pack_size")])
body_size = self["/next_hdr/encoded_hdr/pack_info/pack_pos"].value
yield RawBytes(self, "compressed_data", body_size, "Compressed data")
# Here we could check if copy method was used to "compress" it,
# but this never happens, so just output "compressed file info"
yield RawBytes(self, "compressed_file_info", pack_size,
"Compressed file information")
size = (self._size//8) - pack_size - body_size
if size > 0:
yield RawBytes(self, "unknown_data", size)
elif "header" in self["/next_hdr"]:
yield RawBytes(self, "compressed_data", self._size//8, "Compressed data")
class StartHeader(FieldSet):
static_size = 160
def createFields(self):
yield textHandler(UInt64(self, "next_hdr_offset",
"Next header offset"), hexadecimal)
yield UInt64(self, "next_hdr_size", "Next header size")
yield textHandler(UInt32(self, "next_hdr_crc",
"Next header CRC"), hexadecimal)
class SignatureHeader(FieldSet):
static_size = 96 + StartHeader.static_size
def createFields(self):
yield Bytes(self, "signature", 6, "Signature Header")
yield UInt8(self, "major_ver", "Archive major version")
yield UInt8(self, "minor_ver", "Archive minor version")
yield textHandler(UInt32(self, "start_hdr_crc",
"Start header CRC"), hexadecimal)
yield StartHeader(self, "start_hdr", "Start header")
class SevenZipParser(Parser):
PARSER_TAGS = {
"id": "7zip",
"category": "archive",
"file_ext": ("7z",),
"mime": (u"application/x-7z-compressed",),
"min_size": 32*8,
"magic": (("7z\xbc\xaf\x27\x1c", 0),),
"description": "Compressed archive in 7z format"
}
endian = LITTLE_ENDIAN
def createFields(self):
yield SignatureHeader(self, "signature", "Signature Header")
yield Body(self, "body_data")
yield NextHeader(self, "next_hdr")
def validate(self):
if self.stream.readBytes(0,6) != "7z\xbc\xaf'\x1c":
return "Invalid signature"
return True
def createContentSize(self):
size = self["/signature/start_hdr/next_hdr_offset"].value
size += self["/signature/start_hdr/next_hdr_size"].value
size += 12 # Signature size
size += 20 # Start header size
return size*8
-124
View File
@@ -1,124 +0,0 @@
"""
Tar archive parser.
Author: Victor Stinner
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet,
Enum, UInt8, SubFile, String, NullBytes)
from hachoir_core.tools import humanFilesize, paddingSize, timestampUNIX
from hachoir_core.endian import BIG_ENDIAN
import re
class FileEntry(FieldSet):
type_name = {
# 48 is "0", 49 is "1", ...
0: u"Normal disk file (old format)",
48: u"Normal disk file",
49: u"Link to previously dumped file",
50: u"Symbolic link",
51: u"Character special file",
52: u"Block special file",
53: u"Directory",
54: u"FIFO special file",
55: u"Contiguous file"
}
def getOctal(self, name):
return self.octal2int(self[name].value)
def getDatetime(self):
"""
Create modification date as Unicode string, may raise ValueError.
"""
timestamp = self.getOctal("mtime")
return timestampUNIX(timestamp)
def createFields(self):
yield String(self, "name", 100, "Name", strip="\0", charset="ISO-8859-1")
yield String(self, "mode", 8, "Mode", strip=" \0", charset="ASCII")
yield String(self, "uid", 8, "User ID", strip=" \0", charset="ASCII")
yield String(self, "gid", 8, "Group ID", strip=" \0", charset="ASCII")
yield String(self, "size", 12, "Size", strip=" \0", charset="ASCII")
yield String(self, "mtime", 12, "Modification time", strip=" \0", charset="ASCII")
yield String(self, "check_sum", 8, "Check sum", strip=" \0", charset="ASCII")
yield Enum(UInt8(self, "type", "Type"), self.type_name)
yield String(self, "lname", 100, "Link name", strip=" \0", charset="ISO-8859-1")
yield String(self, "magic", 8, "Magic", strip=" \0", charset="ASCII")
yield String(self, "uname", 32, "User name", strip=" \0", charset="ISO-8859-1")
yield String(self, "gname", 32, "Group name", strip=" \0", charset="ISO-8859-1")
yield String(self, "devmajor", 8, "Dev major", strip=" \0", charset="ASCII")
yield String(self, "devminor", 8, "Dev minor", strip=" \0", charset="ASCII")
yield NullBytes(self, "padding", 167, "Padding (zero)")
filesize = self.getOctal("size")
if filesize:
yield SubFile(self, "content", filesize, filename=self["name"].value)
size = paddingSize(self.current_size//8, 512)
if size:
yield NullBytes(self, "padding_end", size, "Padding (512 align)")
def convertOctal(self, chunk):
return self.octal2int(chunk.value)
def isEmpty(self):
return self["name"].value == ""
def octal2int(self, text):
try:
return int(text, 8)
except ValueError:
return 0
def createDescription(self):
if self.isEmpty():
desc = "(terminator, empty header)"
else:
filename = self["name"].value
filesize = humanFilesize(self.getOctal("size"))
desc = "(%s: %s, %s)" % \
(filename, self["type"].display, filesize)
return "Tar File " + desc
class TarFile(Parser):
endian = BIG_ENDIAN
PARSER_TAGS = {
"id": "tar",
"category": "archive",
"file_ext": ("tar",),
"mime": (u"application/x-tar", u"application/x-gtar"),
"min_size": 512*8,
"magic": (("ustar \0", 257*8),),
"subfile": "skip",
"description": "TAR archive",
}
_sign = re.compile("ustar *\0|[ \0]*$")
def validate(self):
if not self._sign.match(self.stream.readBytes(257*8, 8)):
return "Invalid magic number"
if self[0].name == "terminator":
return "Don't contain any file"
try:
int(self["file[0]/uid"].value, 8)
int(self["file[0]/gid"].value, 8)
int(self["file[0]/size"].value, 8)
except ValueError:
return "Invalid file size"
return True
def createFields(self):
while not self.eof:
field = FileEntry(self, "file[]")
if field.isEmpty():
yield NullBytes(self, "terminator", 512)
break
yield field
if self.current_size < self._size:
yield self.seekBit(self._size, "end")
def createContentSize(self):
return self["terminator"].address + self["terminator"].size
-427
View File
@@ -1,427 +0,0 @@
"""
Zip splitter.
Status: can read most important headers
Authors: Christophe Gisquet and Victor Stinner
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet, ParserError,
Bit, Bits, Enum,
TimeDateMSDOS32, SubFile,
UInt8, UInt16, UInt32, UInt64,
String, PascalString16,
RawBytes)
from hachoir_core.text_handler import textHandler, filesizeHandler, hexadecimal
from hachoir_core.error import HACHOIR_ERRORS
from hachoir_core.tools import makeUnicode
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_parser.common.deflate import Deflate
MAX_FILESIZE = 1000 * 1024 * 1024
COMPRESSION_DEFLATE = 8
COMPRESSION_METHOD = {
0: u"no compression",
1: u"Shrunk",
2: u"Reduced (factor 1)",
3: u"Reduced (factor 2)",
4: u"Reduced (factor 3)",
5: u"Reduced (factor 4)",
6: u"Imploded",
7: u"Tokenizing",
8: u"Deflate",
9: u"Deflate64",
10: u"PKWARE Imploding",
11: u"Reserved by PKWARE",
12: u"File is compressed using BZIP2 algorithm",
13: u"Reserved by PKWARE",
14: u"LZMA (EFS)",
15: u"Reserved by PKWARE",
16: u"Reserved by PKWARE",
17: u"Reserved by PKWARE",
18: u"File is compressed using IBM TERSE (new)",
19: u"IBM LZ77 z Architecture (PFS)",
98: u"PPMd version I, Rev 1",
}
def ZipRevision(field):
return "%u.%u" % divmod(field.value, 10)
class ZipVersion(FieldSet):
static_size = 16
HOST_OS = {
0: u"FAT file system (DOS, OS/2, NT)",
1: u"Amiga",
2: u"VMS (VAX or Alpha AXP)",
3: u"Unix",
4: u"VM/CMS",
5: u"Atari",
6: u"HPFS file system (OS/2, NT 3.x)",
7: u"Macintosh",
8: u"Z-System",
9: u"CP/M",
10: u"TOPS-20",
11: u"NTFS file system (NT)",
12: u"SMS/QDOS",
13: u"Acorn RISC OS",
14: u"VFAT file system (Win95, NT)",
15: u"MVS",
16: u"BeOS (BeBox or PowerMac)",
17: u"Tandem",
}
def createFields(self):
yield textHandler(UInt8(self, "zip_version", "ZIP version"), ZipRevision)
yield Enum(UInt8(self, "host_os", "ZIP Host OS"), self.HOST_OS)
class ZipGeneralFlags(FieldSet):
static_size = 16
def createFields(self):
# Need the compression info from the parent, and that is the byte following
method = self.stream.readBits(self.absolute_address+16, 16, LITTLE_ENDIAN)
yield Bits(self, "unused[]", 2, "Unused")
yield Bit(self, "encrypted_central_dir", "Selected data values in the Local Header are masked")
yield Bit(self, "incomplete", "Reserved by PKWARE for enhanced compression.")
yield Bit(self, "uses_unicode", "Filename and comments are in UTF-8")
yield Bits(self, "unused[]", 4, "Unused")
yield Bit(self, "strong_encrypt", "Strong encryption (version >= 50)")
yield Bit(self, "is_patched", "File is compressed with patched data?")
yield Bit(self, "enhanced_deflate", "Reserved for use with method 8")
yield Bit(self, "has_descriptor",
"Compressed data followed by descriptor?")
if method == 6:
yield Bit(self, "use_8k_sliding", "Use 8K sliding dictionary (instead of 4K)")
yield Bit(self, "use_3shannon", "Use a 3 Shannon-Fano tree (instead of 2 Shannon-Fano)")
elif method in (8, 9):
NAME = {
0: "Normal compression",
1: "Maximum compression",
2: "Fast compression",
3: "Super Fast compression"
}
yield Enum(Bits(self, "method", 2), NAME)
elif method == 14: #LZMA
yield Bit(self, "lzma_eos", "LZMA stream is ended with a EndOfStream marker")
yield Bit(self, "unused[]")
else:
yield Bits(self, "compression_info", 2)
yield Bit(self, "is_encrypted", "File is encrypted?")
class ExtraField(FieldSet):
EXTRA_FIELD_ID = {
0x0007: "AV Info",
0x0009: "OS/2 extended attributes (also Info-ZIP)",
0x000a: "PKWARE Win95/WinNT FileTimes", # undocumented!
0x000c: "PKWARE VAX/VMS (also Info-ZIP)",
0x000d: "PKWARE Unix",
0x000f: "Patch Descriptor",
0x07c8: "Info-ZIP Macintosh (old, J. Lee)",
0x2605: "ZipIt Macintosh (first version)",
0x2705: "ZipIt Macintosh v 1.3.5 and newer (w/o full filename)",
0x334d: "Info-ZIP Macintosh (new, D. Haase Mac3 field)",
0x4341: "Acorn/SparkFS (David Pilling)",
0x4453: "Windows NT security descriptor (binary ACL)",
0x4704: "VM/CMS",
0x470f: "MVS",
0x4b46: "FWKCS MD5 (third party, see below)",
0x4c41: "OS/2 access control list (text ACL)",
0x4d49: "Info-ZIP VMS (VAX or Alpha)",
0x5356: "AOS/VS (binary ACL)",
0x5455: "extended timestamp",
0x5855: "Info-ZIP Unix (original; also OS/2, NT, etc.)",
0x6542: "BeOS (BeBox, PowerMac, etc.)",
0x756e: "ASi Unix",
0x7855: "Info-ZIP Unix (new)",
0xfb4a: "SMS/QDOS",
}
def createFields(self):
yield Enum(UInt16(self, "field_id", "Extra field ID"),
self.EXTRA_FIELD_ID)
size = UInt16(self, "field_data_size", "Extra field data size")
yield size
if size.value > 0:
yield RawBytes(self, "field_data", size, "Unknown field data")
def ZipStartCommonFields(self):
yield ZipVersion(self, "version_needed", "Version needed")
yield ZipGeneralFlags(self, "flags", "General purpose flag")
yield Enum(UInt16(self, "compression", "Compression method"),
COMPRESSION_METHOD)
yield TimeDateMSDOS32(self, "last_mod", "Last modification file time")
yield textHandler(UInt32(self, "crc32", "CRC-32"), hexadecimal)
yield UInt32(self, "compressed_size", "Compressed size")
yield UInt32(self, "uncompressed_size", "Uncompressed size")
yield UInt16(self, "filename_length", "Filename length")
yield UInt16(self, "extra_length", "Extra fields length")
def zipGetCharset(self):
if self["flags/uses_unicode"].value:
return "UTF-8"
else:
return "ISO-8859-15"
class ZipCentralDirectory(FieldSet):
HEADER = 0x02014b50
def createFields(self):
yield ZipVersion(self, "version_made_by", "Version made by")
for field in ZipStartCommonFields(self):
yield field
# Check unicode status
charset = zipGetCharset(self)
yield UInt16(self, "comment_length", "Comment length")
yield UInt16(self, "disk_number_start", "Disk number start")
yield UInt16(self, "internal_attr", "Internal file attributes")
yield UInt32(self, "external_attr", "External file attributes")
yield UInt32(self, "offset_header", "Relative offset of local header")
yield String(self, "filename", self["filename_length"].value,
"Filename", charset=charset)
if 0 < self["extra_length"].value:
yield RawBytes(self, "extra", self["extra_length"].value,
"Extra fields")
if 0 < self["comment_length"].value:
yield String(self, "comment", self["comment_length"].value,
"Comment", charset=charset)
def createDescription(self):
return "Central directory: %s" % self["filename"].display
class Zip64EndCentralDirectory(FieldSet):
HEADER = 0x06064b50
def createFields(self):
yield UInt64(self, "zip64_end_size",
"Size of zip64 end of central directory record")
yield ZipVersion(self, "version_made_by", "Version made by")
yield ZipVersion(self, "version_needed", "Version needed to extract")
yield UInt32(self, "number_disk", "Number of this disk")
yield UInt32(self, "number_disk2",
"Number of the disk with the start of the central directory")
yield UInt64(self, "number_entries",
"Total number of entries in the central directory on this disk")
yield UInt64(self, "number_entries2",
"Total number of entries in the central directory")
yield UInt64(self, "size", "Size of the central directory")
yield UInt64(self, "offset", "Offset of start of central directory")
if 0 < self["zip64_end_size"].value:
yield RawBytes(self, "data_sector", self["zip64_end_size"].value,
"zip64 extensible data sector")
class ZipEndCentralDirectory(FieldSet):
HEADER = 0x06054b50
def createFields(self):
yield UInt16(self, "number_disk", "Number of this disk")
yield UInt16(self, "number_disk2", "Number in the central dir")
yield UInt16(self, "total_number_disk",
"Total number of entries in this disk")
yield UInt16(self, "total_number_disk2",
"Total number of entries in the central dir")
yield UInt32(self, "size", "Size of the central directory")
yield UInt32(self, "offset", "Offset of start of central directory")
yield PascalString16(self, "comment", "ZIP comment")
class ZipDataDescriptor(FieldSet):
HEADER_STRING = "\x50\x4B\x07\x08"
HEADER = 0x08074B50
static_size = 96
def createFields(self):
yield textHandler(UInt32(self, "file_crc32",
"Checksum (CRC32)"), hexadecimal)
yield filesizeHandler(UInt32(self, "file_compressed_size",
"Compressed size (bytes)"))
yield filesizeHandler(UInt32(self, "file_uncompressed_size",
"Uncompressed size (bytes)"))
class FileEntry(FieldSet):
HEADER = 0x04034B50
filename = None
def data(self, size):
compression = self["compression"].value
if compression == 0:
return SubFile(self, "data", size, filename=self.filename)
compressed = SubFile(self, "compressed_data", size, filename=self.filename)
if compression == COMPRESSION_DEFLATE:
return Deflate(compressed)
else:
return compressed
def resync(self):
# Non-seekable output, search the next data descriptor
size = self.stream.searchBytesLength(ZipDataDescriptor.HEADER_STRING, False,
self.absolute_address+self.current_size)
if size <= 0:
raise ParserError("Couldn't resync to %s" %
ZipDataDescriptor.HEADER_STRING)
yield self.data(size)
yield textHandler(UInt32(self, "header[]", "Header"), hexadecimal)
data_desc = ZipDataDescriptor(self, "data_desc", "Data descriptor")
#self.info("Resynced!")
yield data_desc
# The above could be checked anytime, but we prefer trying parsing
# than aborting
if self["crc32"].value == 0 and \
data_desc["file_compressed_size"].value != size:
raise ParserError("Bad resync: position=>%i but data_desc=>%i" %
(size, data_desc["file_compressed_size"].value))
def createFields(self):
for field in ZipStartCommonFields(self):
yield field
length = self["filename_length"].value
if length:
filename = String(self, "filename", length, "Filename",
charset=zipGetCharset(self))
yield filename
self.filename = filename.value
if self["extra_length"].value:
yield RawBytes(self, "extra", self["extra_length"].value, "Extra")
size = self["compressed_size"].value
if size > 0:
yield self.data(size)
elif self["flags/incomplete"].value:
for field in self.resync():
yield field
if self["flags/has_descriptor"].value:
yield ZipDataDescriptor(self, "data_desc", "Data descriptor")
def createDescription(self):
return "File entry: %s (%s)" % \
(self["filename"].value, self["compressed_size"].display)
def validate(self):
if self["compression"].value not in COMPRESSION_METHOD:
return "Unknown compression method (%u)" % self["compression"].value
return ""
class ZipSignature(FieldSet):
HEADER = 0x05054B50
def createFields(self):
yield PascalString16(self, "signature", "Signature")
class Zip64EndCentralDirectoryLocator(FieldSet):
HEADER = 0x07064b50
def createFields(self):
yield UInt32(self, "disk_number", \
"Number of the disk with the start of the zip64 end of central directory")
yield UInt64(self, "relative_offset", \
"Relative offset of the zip64 end of central directory record")
yield UInt32(self, "disk_total_number", "Total number of disks")
class ZipFile(Parser):
endian = LITTLE_ENDIAN
MIME_TYPES = {
# Default ZIP archive
u"application/zip": "zip",
u"application/x-zip": "zip",
# Java archive (JAR)
u"application/x-jar": "jar",
u"application/java-archive": "jar",
# OpenOffice 1.0
u"application/vnd.sun.xml.calc": "sxc",
u"application/vnd.sun.xml.draw": "sxd",
u"application/vnd.sun.xml.impress": "sxi",
u"application/vnd.sun.xml.writer": "sxw",
u"application/vnd.sun.xml.math": "sxm",
# OpenOffice 1.0 (template)
u"application/vnd.sun.xml.calc.template": "stc",
u"application/vnd.sun.xml.draw.template": "std",
u"application/vnd.sun.xml.impress.template": "sti",
u"application/vnd.sun.xml.writer.template": "stw",
u"application/vnd.sun.xml.writer.global": "sxg",
# OpenDocument
u"application/vnd.oasis.opendocument.chart": "odc",
u"application/vnd.oasis.opendocument.image": "odi",
u"application/vnd.oasis.opendocument.database": "odb",
u"application/vnd.oasis.opendocument.formula": "odf",
u"application/vnd.oasis.opendocument.graphics": "odg",
u"application/vnd.oasis.opendocument.presentation": "odp",
u"application/vnd.oasis.opendocument.spreadsheet": "ods",
u"application/vnd.oasis.opendocument.text": "odt",
u"application/vnd.oasis.opendocument.text-master": "odm",
# OpenDocument (template)
u"application/vnd.oasis.opendocument.graphics-template": "otg",
u"application/vnd.oasis.opendocument.presentation-template": "otp",
u"application/vnd.oasis.opendocument.spreadsheet-template": "ots",
u"application/vnd.oasis.opendocument.text-template": "ott",
}
PARSER_TAGS = {
"id": "zip",
"category": "archive",
"file_ext": tuple(MIME_TYPES.itervalues()),
"mime": tuple(MIME_TYPES.iterkeys()),
"magic": (("PK\3\4", 0),),
"subfile": "skip",
"min_size": (4 + 26)*8, # header + file entry
"description": "ZIP archive"
}
def validate(self):
if self["header[0]"].value != FileEntry.HEADER:
return "Invalid magic"
try:
file0 = self["file[0]"]
except HACHOIR_ERRORS, err:
return "Unable to get file #0"
err = file0.validate()
if err:
return "File #0: %s" % err
return True
def createFields(self):
# File data
self.signature = None
self.central_directory = []
while not self.eof:
header = textHandler(UInt32(self, "header[]", "Header"), hexadecimal)
yield header
header = header.value
if header == FileEntry.HEADER:
yield FileEntry(self, "file[]")
elif header == ZipDataDescriptor.HEADER:
yield ZipDataDescriptor(self, "spanning[]")
elif header == 0x30304b50:
yield ZipDataDescriptor(self, "temporary_spanning[]")
elif header == ZipCentralDirectory.HEADER:
yield ZipCentralDirectory(self, "central_directory[]")
elif header == ZipEndCentralDirectory.HEADER:
yield ZipEndCentralDirectory(self, "end_central_directory", "End of central directory")
elif header == Zip64EndCentralDirectory.HEADER:
yield Zip64EndCentralDirectory(self, "end64_central_directory", "ZIP64 end of central directory")
elif header == ZipSignature.HEADER:
yield ZipSignature(self, "signature", "Signature")
elif header == Zip64EndCentralDirectoryLocator.HEADER:
yield Zip64EndCentralDirectoryLocator(self, "end_locator", "ZIP64 Enf of central directory locator")
else:
raise ParserError("Error, unknown ZIP header (0x%08X)." % header)
def createMimeType(self):
if self["file[0]/filename"].value == "mimetype":
return makeUnicode(self["file[0]/data"].value)
else:
return u"application/zip"
def createFilenameSuffix(self):
if self["file[0]/filename"].value == "mimetype":
mime = self["file[0]/compressed_data"].value
if mime in self.MIME_TYPES:
return "." + self.MIME_TYPES[mime]
return ".zip"
def createContentSize(self):
start = 0
end = MAX_FILESIZE * 8
end = self.stream.searchBytes("PK\5\6", start, end)
if end is not None:
return end + 22*8
return None
-12
View File
@@ -1,12 +0,0 @@
from hachoir_parser.audio.aiff import AiffFile
from hachoir_parser.audio.au import AuFile
from hachoir_parser.audio.itunesdb import ITunesDBFile
from hachoir_parser.audio.midi import MidiFile
from hachoir_parser.audio.mpeg_audio import MpegAudioFile
from hachoir_parser.audio.real_audio import RealAudioFile
from hachoir_parser.audio.xm import XMModule
from hachoir_parser.audio.s3m import S3MModule
from hachoir_parser.audio.s3m import PTMModule
from hachoir_parser.audio.mod import AmigaModule
from hachoir_parser.audio.flac import FlacParser
-127
View File
@@ -1,127 +0,0 @@
"""
Audio Interchange File Format (AIFF) parser.
Author: Victor Stinner
Creation: 27 december 2006
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet,
UInt16, UInt32, Float80, TimestampMac32,
RawBytes, NullBytes,
String, Enum, PascalString32)
from hachoir_core.endian import BIG_ENDIAN
from hachoir_core.text_handler import filesizeHandler
from hachoir_core.tools import alignValue
from hachoir_parser.audio.id3 import ID3v2
CODEC_NAME = {
'ACE2': u"ACE 2-to-1",
'ACE8': u"ACE 8-to-3",
'MAC3': u"MAC 3-to-1",
'MAC6': u"MAC 6-to-1",
'NONE': u"None",
'sowt': u"Little-endian, no compression",
}
class Comment(FieldSet):
def createFields(self):
yield TimestampMac32(self, "timestamp")
yield PascalString32(self, "text")
def parseText(self):
yield String(self, "text", self["size"].value)
def parseID3(self):
yield ID3v2(self, "id3v2", size=self["size"].value*8)
def parseComment(self):
yield UInt16(self, "nb_comment")
for index in xrange(self["nb_comment"].value):
yield Comment(self, "comment[]")
def parseCommon(self):
yield UInt16(self, "nb_channel")
yield UInt32(self, "nb_sample")
yield UInt16(self, "sample_size")
yield Float80(self, "sample_rate")
yield Enum(String(self, "codec", 4, strip="\0", charset="ASCII"), CODEC_NAME)
def parseVersion(self):
yield TimestampMac32(self, "timestamp")
def parseSound(self):
yield UInt32(self, "offset")
yield UInt32(self, "block_size")
size = (self.size - self.current_size) // 8
if size:
yield RawBytes(self, "data", size)
class Chunk(FieldSet):
TAG_INFO = {
'COMM': ('common', "Common chunk", parseCommon),
'COMT': ('comment', "Comment", parseComment),
'NAME': ('name', "Name", parseText),
'AUTH': ('author', "Author", parseText),
'FVER': ('version', "Version", parseVersion),
'SSND': ('sound', "Sound data", parseSound),
'ID3 ': ('id3', "ID3", parseID3),
}
def __init__(self, *args):
FieldSet.__init__(self, *args)
self._size = (8 + alignValue(self["size"].value, 2)) * 8
tag = self["type"].value
if tag in self.TAG_INFO:
self._name, self._description, self._parser = self.TAG_INFO[tag]
else:
self._parser = None
def createFields(self):
yield String(self, "type", 4, "Signature (FORM)", charset="ASCII")
yield filesizeHandler(UInt32(self, "size"))
size = self["size"].value
if size:
if self._parser:
for field in self._parser(self):
yield field
if size % 2:
yield NullBytes(self, "padding", 1)
else:
yield RawBytes(self, "data", size)
class AiffFile(Parser):
PARSER_TAGS = {
"id": "aiff",
"category": "audio",
"file_ext": ("aif", "aiff", "aifc"),
"mime": (u"audio/x-aiff",),
"magic_regex": (("FORM.{4}AIF[CF]", 0),),
"min_size": 12*8,
"description": "Audio Interchange File Format (AIFF)"
}
endian = BIG_ENDIAN
def validate(self):
if self.stream.readBytes(0, 4) != "FORM":
return "Invalid signature"
if self.stream.readBytes(8*8, 4) not in ("AIFF", "AIFC"):
return "Invalid type"
return True
def createFields(self):
yield String(self, "signature", 4, "Signature (FORM)", charset="ASCII")
yield filesizeHandler(UInt32(self, "filesize"))
yield String(self, "type", 4, "Form type (AIFF or AIFC)", charset="ASCII")
while not self.eof:
yield Chunk(self, "chunk[]")
def createDescription(self):
if self["type"].value == "AIFC":
return "Audio Interchange File Format Compressed (AIFC)"
else:
return "Audio Interchange File Format (AIFF)"
def createContentSize(self):
return self["filesize"].value * 8
-88
View File
@@ -1,88 +0,0 @@
"""
AU audio file parser
Author: Victor Stinner
Creation: 12 july 2006
"""
from hachoir_parser import Parser
from hachoir_core.field import UInt32, Enum, String, RawBytes
from hachoir_core.endian import BIG_ENDIAN
from hachoir_core.text_handler import displayHandler, filesizeHandler
from hachoir_core.tools import createDict, humanFrequency
class AuFile(Parser):
PARSER_TAGS = {
"id": "sun_next_snd",
"category": "audio",
"file_ext": ("au", "snd"),
"mime": (u"audio/basic",),
"min_size": 24*8,
"magic": ((".snd", 0),),
"description": "Sun/NeXT audio"
}
endian = BIG_ENDIAN
CODEC_INFO = {
1: (8, u"8-bit ISDN u-law"),
2: (8, u"8-bit linear PCM"),
3: (16, u"16-bit linear PCM"),
4: (24, u"24-bit linear PCM"),
5: (32, u"32-bit linear PCM"),
6: (32, u"32-bit IEEE floating point"),
7: (64, u"64-bit IEEE floating point"),
8: (None, u"Fragmented sample data"),
9: (None, u"DSP program"),
10: (8, u"8-bit fixed point"),
11: (16, u"16-bit fixed point"),
12: (24, u"24-bit fixed point"),
13: (32, u"32-bit fixed point"),
18: (16, u"16-bit linear with emphasis"),
19: (16, u"16-bit linear compressed"),
20: (16, u"16-bit linear with emphasis and compression"),
21: (None, u"Music kit DSP commands"),
23: (None, u"4-bit ISDN u-law compressed (CCITT G.721 ADPCM)"),
24: (None, u"ITU-T G.722 ADPCM"),
25: (None, u"ITU-T G.723 3-bit ADPCM"),
26: (None, u"ITU-T G.723 5-bit ADPCM"),
27: (8, u"8-bit ISDN A-law"),
}
# Create bit rate and codec name dictionnaries
BITS_PER_SAMPLE = createDict(CODEC_INFO, 0)
CODEC_NAME = createDict(CODEC_INFO, 1)
VALID_NB_CHANNEL = set((1,2)) # FIXME: 4, 5, 7, 8 channels are supported?
def validate(self):
if self.stream.readBytes(0, 4) != ".snd":
return "Wrong file signature"
if self["channels"].value not in self.VALID_NB_CHANNEL:
return "Invalid number of channel"
return True
def getBitsPerSample(self):
"""
Get bit rate (number of bit per sample per channel),
may returns None if you unable to compute it.
"""
return self.BITS_PER_SAMPLE.get(self["codec"].value)
def createFields(self):
yield String(self, "signature", 4, 'Format signature (".snd")', charset="ASCII")
yield UInt32(self, "data_ofs", "Data offset")
yield filesizeHandler(UInt32(self, "data_size", "Data size"))
yield Enum(UInt32(self, "codec", "Audio codec"), self.CODEC_NAME)
yield displayHandler(UInt32(self, "sample_rate", "Number of samples/second"), humanFrequency)
yield UInt32(self, "channels", "Number of interleaved channels")
size = self["data_ofs"].value - self.current_size // 8
if 0 < size:
yield String(self, "info", size, "Information", strip=" \0", charset="ISO-8859-1")
size = min(self["data_size"].value, (self.size - self.current_size) // 8)
yield RawBytes(self, "audio_data", size, "Audio data")
def createContentSize(self):
return (self["data_ofs"].value + self["data_size"].value) * 8
-157
View File
@@ -1,157 +0,0 @@
"""
FLAC (audio) parser
Documentation:
* http://flac.sourceforge.net/format.html
Author: Esteban Loiseau <baal AT tuxfamily.org>
Creation date: 2008-04-09
"""
from hachoir_parser import Parser
from hachoir_core.field import FieldSet, String, Bit, Bits, UInt16, UInt24, RawBytes, Enum, NullBytes
from hachoir_core.stream import BIG_ENDIAN, LITTLE_ENDIAN
from hachoir_core.tools import createDict
from hachoir_parser.container.ogg import parseVorbisComment
class VorbisComment(FieldSet):
endian = LITTLE_ENDIAN
createFields = parseVorbisComment
class StreamInfo(FieldSet):
static_size = 34*8
def createFields(self):
yield UInt16(self, "min_block_size", "The minimum block size (in samples) used in the stream")
yield UInt16(self, "max_block_size", "The maximum block size (in samples) used in the stream")
yield UInt24(self, "min_frame_size", "The minimum frame size (in bytes) used in the stream")
yield UInt24(self, "max_frame_size", "The maximum frame size (in bytes) used in the stream")
yield Bits(self, "sample_hertz", 20, "Sample rate in Hertz")
yield Bits(self, "nb_channel", 3, "Number of channels minus one")
yield Bits(self, "bits_per_sample", 5, "Bits per sample minus one")
yield Bits(self, "total_samples", 36, "Total samples in stream")
yield RawBytes(self, "md5sum", 16, "MD5 signature of the unencoded audio data")
class SeekPoint(FieldSet):
def createFields(self):
yield Bits(self, "sample_number", 64, "Sample number")
yield Bits(self, "offset", 64, "Offset in bytes")
yield Bits(self, "nb_sample", 16)
class SeekTable(FieldSet):
def createFields(self):
while not self.eof:
yield SeekPoint(self, "point[]")
class MetadataBlock(FieldSet):
"Metadata block field: http://flac.sourceforge.net/format.html#metadata_block"
BLOCK_TYPES = {
0: ("stream_info", u"Stream info", StreamInfo),
1: ("padding[]", u"Padding", None),
2: ("application[]", u"Application", None),
3: ("seek_table", u"Seek table", SeekTable),
4: ("comment", u"Vorbis comment", VorbisComment),
5: ("cue_sheet[]", u"Cue sheet", None),
6: ("picture[]", u"Picture", None),
}
BLOCK_TYPE_DESC = createDict(BLOCK_TYPES, 1)
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = 32 + self["metadata_length"].value * 8
try:
key = self["block_type"].value
self._name, self._description, self.handler = self.BLOCK_TYPES[key]
except KeyError:
self.handler = None
def createFields(self):
yield Bit(self, "last_metadata_block", "True if this is the last metadata block")
yield Enum(Bits(self, "block_type", 7, "Metadata block header type"), self.BLOCK_TYPE_DESC)
yield UInt24(self, "metadata_length", "Length of following metadata in bytes (doesn't include this header)")
block_type = self["block_type"].value
size = self["metadata_length"].value
if not size:
return
try:
handler = self.BLOCK_TYPES[block_type][2]
except KeyError:
handler = None
if handler:
yield handler(self, "content", size=size*8)
elif self["block_type"].value == 1:
yield NullBytes(self, "padding", size)
else:
yield RawBytes(self, "rawdata", size)
class Metadata(FieldSet):
def createFields(self):
while not self.eof:
field = MetadataBlock(self,"metadata_block[]")
yield field
if field["last_metadata_block"].value:
break
class Frame(FieldSet):
SAMPLE_RATES = {
0: "get from STREAMINFO metadata block",
1: "88.2kHz",
2: "176.4kHz",
3: "192kHz",
4: "8kHz",
5: "16kHz",
6: "22.05kHz",
7: "24kHz",
8: "32kHz",
9: "44.1kHz",
10: "48kHz",
11: "96kHz",
12: "get 8 bit sample rate (in kHz) from end of header",
13: "get 16 bit sample rate (in Hz) from end of header",
14: "get 16 bit sample rate (in tens of Hz) from end of header",
}
def createFields(self):
yield Bits(self, "sync", 14, "Sync code: 11111111111110")
yield Bit(self, "reserved[]")
yield Bit(self, "blocking_strategy")
yield Bits(self, "block_size", 4)
yield Enum(Bits(self, "sample_rate", 4), self.SAMPLE_RATES)
yield Bits(self, "channel_assign", 4)
yield Bits(self, "sample_size", 3)
yield Bit(self, "reserved[]")
# FIXME: Finish frame header parser
class Frames(FieldSet):
def createFields(self):
while not self.eof:
yield Frame(self, "frame[]")
# FIXME: Parse all frames
return
class FlacParser(Parser):
"Parse FLAC audio files: FLAC is a lossless audio codec"
MAGIC = "fLaC\x00"
PARSER_TAGS = {
"id": "flac",
"category": "audio",
"file_ext": ("flac",),
"mime": (u"audio/x-flac",),
"magic": ((MAGIC, 0),),
"min_size": 4*8,
"description": "FLAC audio",
}
endian = BIG_ENDIAN
def validate(self):
if self.stream.readBytes(0, len(self.MAGIC)) != self.MAGIC:
return u"Invalid magic string"
return True
def createFields(self):
yield String(self, "signature", 4,charset="ASCII", description="FLAC signature: fLaC string")
yield Metadata(self,"metadata")
yield Frames(self,"frames")
-507
View File
@@ -1,507 +0,0 @@
"""
ID3 metadata parser, supported versions: 1.O, 2.2, 2.3 and 2.4
Informations: http://www.id3.org/
Author: Victor Stinner
"""
from hachoir_core.field import (FieldSet, MatchError, ParserError,
Enum, UInt8, UInt24, UInt32,
CString, String, RawBytes,
Bit, Bits, NullBytes, NullBits)
from hachoir_core.text_handler import textHandler
from hachoir_core.tools import humanDuration
from hachoir_core.endian import NETWORK_ENDIAN
class ID3v1(FieldSet):
static_size = 128 * 8
GENRE_NAME = {
0: u"Blues",
1: u"Classic Rock",
2: u"Country",
3: u"Dance",
4: u"Disco",
5: u"Funk",
6: u"Grunge",
7: u"Hip-Hop",
8: u"Jazz",
9: u"Metal",
10: u"New Age",
11: u"Oldies",
12: u"Other",
13: u"Pop",
14: u"R&B",
15: u"Rap",
16: u"Reggae",
17: u"Rock",
18: u"Techno",
19: u"Industrial",
20: u"Alternative",
21: u"Ska",
22: u"Death Metal",
23: u"Pranks",
24: u"Soundtrack",
25: u"Euro-Techno",
26: u"Ambient",
27: u"Trip-Hop",
28: u"Vocal",
29: u"Jazz+Funk",
30: u"Fusion",
31: u"Trance",
32: u"Classical",
33: u"Instrumental",
34: u"Acid",
35: u"House",
36: u"Game",
37: u"Sound Clip",
38: u"Gospel",
39: u"Noise",
40: u"AlternRock",
41: u"Bass",
42: u"Soul",
43: u"Punk",
44: u"Space",
45: u"Meditative",
46: u"Instrumental Pop",
47: u"Instrumental Rock",
48: u"Ethnic",
49: u"Gothic",
50: u"Darkwave",
51: u"Techno-Industrial",
52: u"Electronic",
53: u"Pop-Folk",
54: u"Eurodance",
55: u"Dream",
56: u"Southern Rock",
57: u"Comedy",
58: u"Cult",
59: u"Gangsta",
60: u"Top 40",
61: u"Christian Rap",
62: u"Pop/Funk",
63: u"Jungle",
64: u"Native American",
65: u"Cabaret",
66: u"New Wave",
67: u"Psychadelic",
68: u"Rave",
69: u"Showtunes",
70: u"Trailer",
71: u"Lo-Fi",
72: u"Tribal",
73: u"Acid Punk",
74: u"Acid Jazz",
75: u"Polka",
76: u"Retro",
77: u"Musical",
78: u"Rock & Roll",
79: u"Hard Rock",
# Following are winamp extentions
80: u"Folk",
81: u"Folk-Rock",
82: u"National Folk",
83: u"Swing",
84: u"Fast Fusion",
85: u"Bebob",
86: u"Latin",
87: u"Revival",
88: u"Celtic",
89: u"Bluegrass",
90: u"Avantgarde",
91: u"Gothic Rock",
92: u"Progressive Rock",
93: u"Psychedelic Rock",
94: u"Symphonic Rock",
95: u"Slow Rock",
96: u"Big Band",
97: u"Chorus",
98: u"Easy Listening",
99: u"Acoustic",
100: u"Humour",
101: u"Speech",
102: u"Chanson",
103: u"Opera",
104: u"Chamber Music",
105: u"Sonata",
106: u"Symphony",
107: u"Booty Bass",
108: u"Primus",
109: u"Porn Groove",
110: u"Satire",
111: u"Slow Jam",
112: u"Club",
113: u"Tango",
114: u"Samba",
115: u"Folklore",
116: u"Ballad",
117: u"Power Ballad",
118: u"Rhythmic Soul",
119: u"Freestyle",
120: u"Duet",
121: u"Punk Rock",
122: u"Drum Solo",
123: u"A capella",
124: u"Euro-House",
125: u"Dance Hall",
126: u"Goa",
127: u"Drum & Bass",
128: u"Club-House",
129: u"Hardcore",
130: u"Terror",
131: u"Indie",
132: u"Britpop",
133: u"Negerpunk",
134: u"Polsk Punk",
135: u"Beat",
136: u"Christian Gangsta Rap",
137: u"Heavy Metal",
138: u"Black Metal",
139: u"Crossover",
140: u"Contemporary Christian",
141: u"Christian Rock ",
142: u"Merengue",
143: u"Salsa",
144: u"Trash Metal",
145: u"Anime",
146: u"JPop",
147: u"Synthpop"
}
def createFields(self):
yield String(self, "signature", 3, "IDv1 signature (\"TAG\")", charset="ASCII")
if self["signature"].value != "TAG":
raise MatchError("Stream doesn't look like ID3v1 (wrong signature)!")
# TODO: Charset of below strings?
yield String(self, "song", 30, "Song title", strip=" \0", charset="ISO-8859-1")
yield String(self, "author", 30, "Author", strip=" \0", charset="ISO-8859-1")
yield String(self, "album", 30, "Album title", strip=" \0", charset="ISO-8859-1")
yield String(self, "year", 4, "Year", strip=" \0", charset="ISO-8859-1")
# TODO: Write better algorithm to guess ID3v1 version
version = self.getVersion()
if version in ("v1.1", "v1.1b"):
if version == "v1.1b":
# ID3 v1.1b
yield String(self, "comment", 29, "Comment", strip=" \0", charset="ISO-8859-1")
yield UInt8(self, "track_nb", "Track number")
else:
# ID3 v1.1
yield String(self, "comment", 30, "Comment", strip=" \0", charset="ISO-8859-1")
yield Enum(UInt8(self, "genre", "Genre"), self.GENRE_NAME)
else:
# ID3 v1.0
yield String(self, "comment", 31, "Comment", strip=" \0", charset="ISO-8859-1")
def getVersion(self):
addr = self.absolute_address + 126*8
bytes = self.stream.readBytes(addr, 2)
# last byte (127) is not space?
if bytes[1] != ' ':
# byte 126 is nul?
if bytes[0] == 0x00:
return "v1.1"
else:
return "v1.1b"
else:
return "1.0"
def createDescription(self):
version = self.getVersion()
return "ID3 %s: author=%s, song=%s" % (
version, self["author"].value, self["song"].value)
def getCharset(field):
try:
key = field.value
return ID3_StringCharset.charset_name[key]
except KeyError:
raise ParserError("ID3v2: Invalid charset (%s)." % key)
class ID3_String(FieldSet):
STRIP = " \0"
def createFields(self):
yield String(self, "text", self._size/8, "Text", charset="ISO-8859-1", strip=self.STRIP)
class ID3_StringCharset(ID3_String):
STRIP = " \0"
charset_desc = {
0: "ISO-8859-1",
1: "UTF-16 with BOM",
2: "UTF-16 (big endian)",
3: "UTF-8"
}
charset_name = {
0: "ISO-8859-1",
1: "UTF-16",
2: "UTF-16-BE",
3: "UTF-8"
}
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
size = (self.size - self.current_size)/8
if not size:
return
charset = getCharset(self["charset"])
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
class ID3_GEOB(ID3_StringCharset):
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
charset = getCharset(self["charset"])
yield CString(self, "mime", "MIME type", charset=charset)
yield CString(self, "filename", "File name", charset=charset)
yield CString(self, "description", "Content description", charset=charset)
size = (self.size - self.current_size) // 8
if not size:
return
yield String(self, "text", size, "Text", charset=charset)
class ID3_Comment(ID3_StringCharset):
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
yield String(self, "lang", 3, "Language", charset="ASCII")
charset = getCharset(self["charset"])
yield CString(self, "title", "Title", charset=charset, strip=self.STRIP)
size = (self.size - self.current_size) // 8
if not size:
return
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
class ID3_StringTitle(ID3_StringCharset):
def createFields(self):
yield Enum(UInt8(self, "charset"), self.charset_desc)
if self.current_size == self.size:
return
charset = getCharset(self["charset"])
yield CString(self, "title", "Title", charset=charset, strip=self.STRIP)
size = (self.size - self.current_size)/8
if not size:
return
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
class ID3_Private(FieldSet):
def createFields(self):
size = self._size/8
# TODO: Strings charset?
if self.stream.readBytes(self.absolute_address, 9) == "PeakValue":
yield String(self, "text", 9, "Text")
size -= 9
yield String(self, "content", size, "Content")
class ID3_TrackLength(FieldSet):
def createFields(self):
yield NullBytes(self, "zero", 1)
yield textHandler(String(self, "length", self._size/8 - 1,
"Length in ms", charset="ASCII"), self.computeLength)
def computeLength(self, field):
try:
ms = int(field.value)
return humanDuration(ms)
except:
return field.value
class ID3_Picture23(FieldSet):
pict_type_name = {
0x00: "Other",
0x01: "32x32 pixels 'file icon' (PNG only)",
0x02: "Other file icon",
0x03: "Cover (front)",
0x04: "Cover (back)",
0x05: "Leaflet page",
0x06: "Media (e.g. lable side of CD)",
0x07: "Lead artist/lead performer/soloist",
0x08: "Artist/performer",
0x09: "Conductor",
0x0A: "Band/Orchestra",
0x0B: "Composer",
0x0C: "Lyricist/text writer",
0x0D: "Recording Location",
0x0E: "During recording",
0x0F: "During performance",
0x10: "Movie/video screen capture",
0x11: "A bright coloured fish",
0x12: "Illustration",
0x13: "Band/artist logotype",
0x14: "Publisher/Studio logotype"
}
def createFields(self):
yield Enum(UInt8(self, "charset"), ID3_StringCharset.charset_desc)
charset = getCharset(self["charset"])
yield String(self, "img_fmt", 3, charset="ASCII")
yield Enum(UInt8(self, "pict_type"), self.pict_type_name)
yield CString(self, "text", "Text", charset=charset, strip=" \0")
size = (self._size - self._current_size) / 8
if size:
yield RawBytes(self, "img_data", size)
class ID3_Picture24(FieldSet):
def createFields(self):
yield Enum(UInt8(self, "charset"), ID3_StringCharset.charset_desc)
charset = getCharset(self["charset"])
yield CString(self, "mime", "MIME type", charset=charset)
yield Enum(UInt8(self, "pict_type"), ID3_Picture23.pict_type_name)
yield CString(self, "description", charset=charset)
size = (self._size - self._current_size) / 8
if size:
yield RawBytes(self, "img_data", size)
class ID3_Chunk(FieldSet):
endian = NETWORK_ENDIAN
tag22_name = {
"TT2": "Track title",
"TP1": "Artist",
"TRK": "Track number",
"COM": "Comment",
"TCM": "Composer",
"TAL": "Album",
"TYE": "Year",
"TEN": "Encoder",
"TCO": "Content type",
"PIC": "Picture"
}
tag23_name = {
"COMM": "Comment",
"GEOB": "Encapsulated object",
"PRIV": "Private",
"TPE1": "Artist",
"TCOP": "Copyright",
"TALB": "Album",
"TENC": "Encoder",
"TYER": "Year",
"TSSE": "Encoder settings",
"TCOM": "Composer",
"TRCK": "Track number",
"PCNT": "Play counter",
"TCON": "Content type",
"TLEN": "Track length",
"TIT2": "Track title",
"WXXX": "User defined URL"
}
handler = {
"COMM": ID3_Comment,
"COM": ID3_Comment,
"GEOB": ID3_GEOB,
"PIC": ID3_Picture23,
"APIC": ID3_Picture24,
"PRIV": ID3_Private,
"TXXX": ID3_StringTitle,
"WOAR": ID3_String,
"WXXX": ID3_StringTitle,
}
def __init__(self, *args):
FieldSet.__init__(self, *args)
if 3 <= self["../ver_major"].value:
self._size = (10 + self["size"].value) * 8
else:
self._size = (self["size"].value + 6) * 8
def createFields(self):
if 3 <= self["../ver_major"].value:
# ID3 v2.3 and 2.4
yield Enum(String(self, "tag", 4, "Tag", charset="ASCII", strip="\0"), ID3_Chunk.tag23_name)
if 4 <= self["../ver_major"].value:
yield ID3_Size(self, "size") # ID3 v2.4
else:
yield UInt32(self, "size") # ID3 v2.3
yield Bit(self, "tag_alter", "Tag alter preservation")
yield Bit(self, "file_alter", "Tag alter preservation")
yield Bit(self, "rd_only", "Read only?")
yield NullBits(self, "padding[]", 5)
yield Bit(self, "compressed", "Frame is compressed?")
yield Bit(self, "encrypted", "Frame is encrypted?")
yield Bit(self, "group", "Grouping identity")
yield NullBits(self, "padding[]", 5)
size = self["size"].value
is_compressed = self["compressed"].value
else:
# ID3 v2.2
yield Enum(String(self, "tag", 3, "Tag", charset="ASCII", strip="\0"), ID3_Chunk.tag22_name)
yield UInt24(self, "size")
size = self["size"].value - self.current_size/8 + 6
is_compressed = False
if size:
cls = None
if not(is_compressed):
tag = self["tag"].value
if tag in ID3_Chunk.handler:
cls = ID3_Chunk.handler[tag]
elif tag[0] == "T":
cls = ID3_StringCharset
if cls:
yield cls(self, "content", "Content", size=size*8)
else:
yield RawBytes(self, "content", size, "Raw data content")
def createDescription(self):
if self["size"].value != 0:
return "ID3 Chunk: %s" % self["tag"].display
else:
return "ID3 Chunk: (terminator)"
class ID3_Size(Bits):
static_size = 32
def __init__(self, parent, name, description=None):
Bits.__init__(self, parent, name, 32, description)
def createValue(self):
data = self.parent.stream.readBytes(self.absolute_address, 4)
# TODO: Check that bit #7 of each byte is nul: not(ord(data[i]) & 127)
return reduce(lambda x, y: x*128 + y, (ord(item) for item in data ))
class ID3v2(FieldSet):
endian = NETWORK_ENDIAN
VALID_MAJOR_VERSIONS = (2, 3, 4)
def __init__(self, parent, name, size=None):
FieldSet.__init__(self, parent, name, size=size)
if not self._size:
self._size = (self["size"].value + 10) * 8
def createDescription(self):
return "ID3 v2.%s.%s" % \
(self["ver_major"].value, self["ver_minor"].value)
def createFields(self):
# Signature + version
yield String(self, "header", 3, "Header (ID3)", charset="ASCII")
yield UInt8(self, "ver_major", "Version (major)")
yield UInt8(self, "ver_minor", "Version (minor)")
# Check format
if self["header"].value != "ID3":
raise MatchError("Signature error, should be \"ID3\".")
if self["ver_major"].value not in self.VALID_MAJOR_VERSIONS \
or self["ver_minor"].value != 0:
raise MatchError(
"Unknown ID3 metadata version (2.%u.%u)"
% (self["ver_major"].value, self["ver_minor"].value))
# Flags
yield Bit(self, "unsync", "Unsynchronisation is used?")
yield Bit(self, "ext", "Extended header is used?")
yield Bit(self, "exp", "Experimental indicator")
yield NullBits(self, "padding[]", 5)
# Size
yield ID3_Size(self, "size")
# All tags
while self.current_size < self._size:
field = ID3_Chunk(self, "field[]")
yield field
if field["size"].value == 0:
break
# Search first byte of the MPEG file
padding = self.seekBit(self._size)
if padding:
yield padding
-433
View File
@@ -1,433 +0,0 @@
"""
iPod iTunesDB parser.
Documentation:
- http://ipodlinux.org/ITunesDB
Author: Romain HERAULT
Creation date: 19 august 2006
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet,
UInt8, UInt16, UInt32, UInt64, TimestampMac32,
String, Float32, NullBytes, Enum)
from hachoir_core.endian import LITTLE_ENDIAN
from hachoir_core.tools import humanDuration
from hachoir_core.text_handler import displayHandler, filesizeHandler
list_order={
1 : "playlist order (manual sort order)",
2 : "???",
3 : "songtitle",
4 : "album",
5 : "artist",
6 : "bitrate",
7 : "genre",
8 : "kind",
9 : "date modified",
10 : "track number",
11 : "size",
12 : "time",
13 : "year",
14 : "sample rate",
15 : "comment",
16 : "date added",
17 : "equalizer",
18 : "composer",
19 : "???",
20 : "play count",
21 : "last played",
22 : "disc number",
23 : "my rating",
24 : "release date",
25 : "BPM",
26 : "grouping",
27 : "category",
28 : "description",
29 : "show",
30 : "season",
31 : "episode number"
}
class DataObject(FieldSet):
type_name={
1:"Title",
2:"Location",
3:"Album",
4:"Artist",
5:"Genre",
6:"Filetype",
7:"EQ Setting",
8:"Comment",
9:"Category",
12:"Composer",
13:"Grouping",
14:"Description text",
15:"Podcast Enclosure URL",
16:"Podcast RSS URL",
17:"Chapter data",
18:"Subtitle",
19:"Show (for TV Shows only)",
20:"Episode",
21:"TV Network",
50:"Smart Playlist Data",
51:"Smart Playlist Rules",
52:"Library Playlist Index",
100:"Column info",
}
mhod52_sort_index_type_name={
3:"Title",
4:"Album, then Disk/Tracknumber, then Title",
5:"Artist, then Album, then Disc/Tracknumber, then Title",
7:"Genre, then Artist, then Album, then Disc/Tracknumber, then Title",
8:"Composer, then Title"
}
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = self["entry_length"].value *8
def createFields(self):
yield String(self, "header_id", 4, "Data Object Header Markup (\"mhod\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "entry_length", "Entry Length")
yield Enum(UInt32(self, "type", "type"),self.type_name)
if(self["type"].value<15):
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "position", "Position")
yield UInt32(self, "length", "String Length in bytes")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield String(self, "string", self["length"].value, "String Data", charset="UTF-16-LE")
elif (self["type"].value<17):
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield String(self, "string", self._size/8-self["header_length"].value, "String Data", charset="UTF-8")
elif (self["type"].value == 52):
yield UInt32(self, "unknown[]", "unk1")
yield UInt32(self, "unknown[]", "unk2")
yield Enum(UInt32(self, "sort_index_type", "Sort Index Type"),self.mhod52_sort_index_type_name)
yield UInt32(self, "entry_count", "Entry Count")
indexes_size = self["entry_count"].value*4
padding_offset = self["entry_length"].value - indexes_size
padding = self.seekByte(padding_offset, "header padding")
if padding:
yield padding
for i in xrange(self["entry_count"].value):
yield UInt32(self, "index["+str(i)+"]", "Index of the "+str(i)+"nth mhit")
else:
padding = self.seekByte(self["header_length"].value, "header padding")
if padding:
yield padding
padding = self.seekBit(self._size, "entry padding")
if padding:
yield padding
class TrackItem(FieldSet):
x1_type_name={
0:"AAC or CBR MP3",
1:"VBR MP3"
}
x2_type_name={
0:"AAC",
1:"MP3"
}
media_type_name={
0x00:"Audio/Video",
0x01:"Audio",
0x02:"Video",
0x04:"Podcast",
0x06:"Video Podcast",
0x08:"Audiobook",
0x20:"Music Video",
0x40:"TV Show",
0X60:"TV Show (Music lists)",
}
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = self["entry_length"].value *8
def createFields(self):
yield String(self, "header_id", 4, "Track Item Header Markup (\"mhit\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "entry_length", "Entry Length")
yield UInt32(self, "string_number", "Number of Strings")
yield UInt32(self, "unique_id", "Unique ID")
yield UInt32(self, "visible_tag", "Visible Tag")
yield String(self, "file_type", 4, "File Type")
yield Enum(UInt8(self, "x1_type", "Extended Type 1"),self.x1_type_name)
yield Enum(UInt8(self, "x2_type", "Extended type 2"),self.x2_type_name)
yield UInt8(self, "compilation_flag", "Compilation Flag")
yield UInt8(self, "rating", "Rating")
yield TimestampMac32(self, "added_date", "Date when the item was added")
yield filesizeHandler(UInt32(self, "size", "Track size in bytes"))
yield displayHandler(UInt32(self, "length", "Track length in milliseconds"), humanDuration)
yield UInt32(self, "track_number", "Number of this track")
yield UInt32(self, "total_track", "Total number of tracks")
yield UInt32(self, "year", "Year of the track")
yield UInt32(self, "bitrate", "Bitrate")
yield UInt32(self, "samplerate", "Sample Rate")
yield UInt32(self, "volume", "volume")
yield UInt32(self, "start_time", "Start playing at, in milliseconds")
yield UInt32(self, "stop_time", "Stop playing at, in milliseconds")
yield UInt32(self, "soundcheck", "SoundCheck preamp")
yield UInt32(self, "playcount_1", "Play count of the track")
yield UInt32(self, "playcount_2", "Play count of the track (identical to playcount_1)")
yield UInt32(self, "last_played_time", "Time the song was last played")
yield UInt32(self, "disc_number", "disc number in multi disc sets")
yield UInt32(self, "total_discs", "Total number of discs in the disc set")
yield UInt32(self, "userid", "User ID in the DRM scheme")
yield TimestampMac32(self, "last_modified", "Time of the last modification of the track")
yield UInt32(self, "bookmark_time", "Bookmark time for AudioBook")
yield UInt64(self, "dbid", "Unique DataBase ID for the song (identical in mhit and in mhii)")
yield UInt8(self, "checked", "song is checked")
yield UInt8(self, "application_rating", "Last Rating before change")
yield UInt16(self, "BPM", "BPM of the track")
yield UInt16(self, "artwork_count", "number of artworks fo this item")
yield UInt16(self, "unknown[]")
yield UInt32(self, "artwork_size", "Total size of artworks in bytes")
yield UInt32(self, "unknown[]")
yield Float32(self, "sample_rate_2", "Sample Rate express in float")
yield UInt32(self, "released_date", "Date of release in Music Store or in Podcast")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt8(self, "has_artwork", "0x01 for track with artwork, 0x02 otherwise")
yield UInt8(self, "skip_wen_shuffling", "Skip that track when shuffling")
yield UInt8(self, "remember_playback_position", "Remember playback position")
yield UInt8(self, "flag4", "Flag 4")
yield UInt64(self, "dbid2", "Unique DataBase ID for the song (identical as above)")
yield UInt8(self, "lyrics_flag", "Lyrics Flag")
yield UInt8(self, "movie_file_flag", "Movie File Flag")
yield UInt8(self, "played_mark", "Track has been played")
yield UInt8(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "sample_count", "Number of samples in the song (only for WAV and AAC files)")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield Enum(UInt32(self, "media_type", "Media Type for video iPod"),self.media_type_name)
yield UInt32(self, "season_number", "Season Number")
yield UInt32(self, "episode_number", "Episode Number")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
yield UInt32(self, "unknown[]")
padding = self.seekByte(self["header_length"].value, "header padding")
if padding:
yield padding
#while ((self.stream.readBytes(0, 4) == 'mhod') and ((self.current_size/8) < self["entry_length"].value)):
for i in xrange(self["string_number"].value):
yield DataObject(self, "data[]")
padding = self.seekBit(self._size, "entry padding")
if padding:
yield padding
class TrackList(FieldSet):
def createFields(self):
yield String(self, "header_id", 4, "Track List Header Markup (\"mhlt\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "track_number", "Number of Tracks")
padding = self.seekByte(self["header_length"].value, "header padding")
if padding:
yield padding
for i in xrange(self["track_number"].value):
yield TrackItem(self, "track[]")
class PlaylistItem(FieldSet):
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = self["entry_length"].value *8
def createFields(self):
yield String(self, "header_id", 4, "Playlist Item Header Markup (\"mhip\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "entry_length", "Entry Length")
yield UInt32(self, "data_object_child_count", "Number of Child Data Objects")
yield UInt32(self, "podcast_grouping_flag", "Podcast Grouping Flag")
yield UInt32(self, "group_id", "Group ID")
yield UInt32(self, "track_id", "Track ID")
yield TimestampMac32(self, "timestamp", "Song Timestamp")
yield UInt32(self, "podcast_grouping_ref", "Podcast Grouping Reference")
padding = self.seekByte(self["header_length"].value, "header padding")
if padding:
yield padding
for i in xrange(self["data_object_child_count"].value):
yield DataObject(self, "mhod[]")
class Playlist(FieldSet):
is_master_pl_name={
0:"Regular playlist",
1:"Master playlist"
}
is_podcast_name={
0:"Normal Playlist List",
1:"Podcast Playlist List"
}
list_sort_order_name={
1:"Manual Sort Order",
2:"???",
3:"Song Title",
4:"Album",
5:"Artist",
6:"Bitrate",
7:"Genre",
8:"Kind",
9:"Date Modified",
10:"Track Number",
11:"Size",
12:"Time",
13:"Year",
14:"Sample Rate",
15:"Comment",
16:"Date Added",
17:"Equalizer",
18:"Composer",
19:"???",
20:"Play Count",
21:"Last Played",
22:"Disc Number",
23:"My Rating",
24:"Release Date",
25:"BPM",
26:"Grouping",
27:"Category",
28:"Description",
29:"Show",
30:"Season",
31:"Episode Number"
}
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = self["entry_length"].value *8
def createFields(self):
yield String(self, "header_id", 4, "Playlist List Header Markup (\"mhyp\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "entry_length", "Entry Length")
yield UInt32(self, "data_object_child_count", "Number of Child Data Objects")
yield UInt32(self, "playlist_count", "Number of Playlist Items")
yield Enum(UInt8(self, "type", "Normal or master playlist?"), self.is_master_pl_name)
yield UInt8(self, "XXX1", "XXX1")
yield UInt8(self, "XXX2", "XXX2")
yield UInt8(self, "XXX3", "XXX3")
yield TimestampMac32(self, "creation_date", "Date when the playlist was created")
yield UInt64(self, "playlistid", "Persistent Playlist ID")
yield UInt32(self, "unk3", "unk3")
yield UInt16(self, "string_mhod_count", "Number of string MHODs for this playlist")
yield Enum(UInt16(self, "is_podcast", "Playlist or Podcast List?"), self.is_podcast_name)
yield Enum(UInt32(self, "sort_order", "Playlist Sort Order"), self.list_sort_order_name)
padding = self.seekByte(self["header_length"].value, "entry padding")
if padding:
yield padding
for i in xrange(self["data_object_child_count"].value):
yield DataObject(self, "mhod[]")
for i in xrange(self["playlist_count"].value):
yield PlaylistItem(self, "playlist_item[]")
class PlaylistList(FieldSet):
def createFields(self):
yield String(self, "header_id", 4, "Playlist List Header Markup (\"mhlp\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "playlist_number", "Number of Playlists")
padding = self.seekByte(self["header_length"].value, "header padding")
if padding:
yield padding
for i in xrange(self["playlist_number"].value):
yield Playlist(self, "playlist[]")
class DataSet(FieldSet):
type_name={
1:"Track List",
2:"Play List",
3:"Podcast List"
}
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = self["entry_length"].value *8
def createFields(self):
yield String(self, "header_id", 4, "DataSet Header Markup (\"mhsd\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "entry_length", "Entry Length")
yield Enum(UInt32(self, "type", "type"),self.type_name)
padding = self.seekByte(self["header_length"].value, "header_raw")
if padding:
yield padding
if self["type"].value == 1:
yield TrackList(self, "tracklist[]")
if self["type"].value == 2:
yield PlaylistList(self, "playlist_list[]");
if self["type"].value == 3:
yield PlaylistList(self, "podcast_list[]");
padding = self.seekBit(self._size, "entry padding")
if padding:
yield padding
class DataBase(FieldSet):
def __init__(self, *args, **kw):
FieldSet.__init__(self, *args, **kw)
self._size = self["entry_length"].value *8
# def createFields(self):
class ITunesDBFile(Parser):
PARSER_TAGS = {
"id": "itunesdb",
"category": "audio",
"min_size": 44*8,
"magic": (('mhbd',0),),
"description": "iPod iTunesDB file"
}
endian = LITTLE_ENDIAN
def validate(self):
return self.stream.readBytes(0, 4) == 'mhbd'
def createFields(self):
yield String(self, "header_id", 4, "DataBase Header Markup (\"mhbd\")", charset="ISO-8859-1")
yield UInt32(self, "header_length", "Header Length")
yield UInt32(self, "entry_length", "Entry Length")
yield UInt32(self, "unknown[]")
yield UInt32(self, "version_number", "Version Number")
yield UInt32(self, "child_number", "Number of Children")
yield UInt64(self, "id", "ID for this database")
yield UInt32(self, "unknown[]")
yield UInt64(self, "initial_dbid", "Initial DBID")
size = self["header_length"].value-self.current_size/ 8
if size>0:
yield NullBytes(self, "padding", size)
for i in xrange(self["child_number"].value):
yield DataSet(self, "dataset[]")
padding = self.seekByte(self["entry_length"].value, "entry padding")
if padding:
yield padding
def createContentSize(self):
return self["entry_length"].value * 8
-246
View File
@@ -1,246 +0,0 @@
"""
Musical Instrument Digital Interface (MIDI) audio file parser.
Documentation:
- Standard MIDI File Format, Dustin Caldwell (downloaded on wotsit.org)
Author: Victor Stinner
Creation: 27 december 2006
"""
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet, Bits, ParserError,
String, UInt32, UInt24, UInt16, UInt8, Enum, RawBits, RawBytes)
from hachoir_core.endian import BIG_ENDIAN
from hachoir_core.text_handler import textHandler, hexadecimal
from hachoir_core.tools import createDict, humanDurationNanosec
from hachoir_parser.common.tracker import NOTE_NAME
MAX_FILESIZE = 10 * 1024 * 1024
class Integer(Bits):
def __init__(self, parent, name, description=None):
Bits.__init__(self, parent, name, 8, description)
stream = parent.stream
addr = self.absolute_address
value = 0
while True:
bits = stream.readBits(addr, 8, parent.endian)
value = (value << 7) + (bits & 127)
if not(bits & 128):
break
addr += 8
self._size += 8
if 32 < self._size:
raise ParserError("Integer size is bigger than 32-bit")
self.createValue = lambda: value
def parseNote(parser):
yield Enum(UInt8(parser, "note", "Note number"), NOTE_NAME)
yield UInt8(parser, "velocity")
def parseControl(parser):
yield UInt8(parser, "control", "Controller number")
yield UInt8(parser, "value", "New value")
def parsePatch(parser):
yield UInt8(parser, "program", "New program number")
def parseChannel(parser, size=1):
yield UInt8(parser, "channel", "Channel number")
def parsePitch(parser):
yield UInt8(parser, "bottom", "(least sig) 7 bits of value")
yield UInt8(parser, "top", "(most sig) 7 bits of value")
def parseText(parser, size):
yield String(parser, "text", size)
def parseSMPTEOffset(parser, size):
yield RawBits(parser, "padding", 1)
yield Enum(Bits(parser, "frame_rate", 2),
{0:"24 fps", 1:"25 fps", 2:"30 fps (drop frame)", 3:"30 fps"})
yield Bits(parser, "hour", 5)
yield UInt8(parser, "minute")
yield UInt8(parser, "second")
yield UInt8(parser, "frame")
yield UInt8(parser, "subframe", "100 subframes per frame")
def formatTempo(field):
return humanDurationNanosec(field.value*1000)
def parseTempo(parser, size):
yield textHandler(UInt24(parser, "microsec_quarter", "Microseconds per quarter note"), formatTempo)
def parseTimeSignature(parser, size):
yield UInt8(parser, "numerator", "Numerator of time signature")
yield UInt8(parser, "denominator", "denominator of time signature 2=quarter 3=eighth, etc.")
yield UInt8(parser, "nb_tick", "Number of ticks in metronome click")
yield UInt8(parser, "nb_32nd_note", "Number of 32nd notes to the quarter note")
class Command(FieldSet):
COMMAND = {}
for channel in xrange(16):
COMMAND[0x80+channel] = ("Note off (channel %u)" % channel, parseNote)
COMMAND[0x90+channel] = ("Note on (channel %u)" % channel, parseNote)
COMMAND[0xA0+channel] = ("Key after-touch (channel %u)" % channel, parseNote)
COMMAND[0xB0+channel] = ("Control change (channel %u)" % channel, parseControl)
COMMAND[0xC0+channel] = ("Program (patch) change (channel %u)" % channel, parsePatch)
COMMAND[0xD0+channel] = ("Channel after-touch (channel %u)" % channel, parseChannel)
COMMAND[0xE0+channel] = ("Pitch wheel change (channel %u)" % channel, parsePitch)
COMMAND_DESC = createDict(COMMAND, 0)
COMMAND_PARSER = createDict(COMMAND, 1)
META_COMMAND_TEXT = 1
META_COMMAND_NAME = 3
META_COMMAND = {
0x00: ("Sets the track's sequence number", None),
0x01: ("Text event", parseText),
0x02: ("Copyright info", parseText),
0x03: ("Sequence or Track name", parseText),
0x04: ("Track instrument name", parseText),
0x05: ("Lyric", parseText),
0x06: ("Marker", parseText),
0x07: ("Cue point", parseText),
0x20: ("MIDI Channel Prefix", parseChannel),
0x2F: ("End of the track", None),
0x51: ("Set tempo", parseTempo),
0x54: ("SMPTE offset", parseSMPTEOffset),
0x58: ("Time Signature", parseTimeSignature),
0x59: ("Key signature", None),
0x7F: ("Sequencer specific information", None),
}
META_COMMAND_DESC = createDict(META_COMMAND, 0)
META_COMMAND_PARSER = createDict(META_COMMAND, 1)
def __init__(self, *args, **kwargs):
if 'prev_command' in kwargs:
self.prev_command = kwargs['prev_command']
del kwargs['prev_command']
else:
self.prev_command = None
self.command = None
FieldSet.__init__(self, *args, **kwargs)
def createFields(self):
yield Integer(self, "time", "Delta time in ticks")
next = self.stream.readBits(self.absolute_address+self.current_size, 8, self.root.endian)
if next & 0x80 == 0:
# "Running Status" command
if self.prev_command is None:
raise ParserError("Running Status command not preceded by another command.")
self.command = self.prev_command.command
else:
yield Enum(textHandler(UInt8(self, "command"), hexadecimal), self.COMMAND_DESC)
self.command = self["command"].value
if self.command == 0xFF:
yield Enum(textHandler(UInt8(self, "meta_command"), hexadecimal), self.META_COMMAND_DESC)
yield UInt8(self, "data_len")
size = self["data_len"].value
if size:
command = self["meta_command"].value
if command in self.META_COMMAND_PARSER:
parser = self.META_COMMAND_PARSER[command]
else:
parser = None
if parser:
for field in parser(self, size):
yield field
else:
yield RawBytes(self, "data", size)
else:
if self.command not in self.COMMAND_PARSER:
raise ParserError("Unknown command: %s" % self["command"].display)
parser = self.COMMAND_PARSER[self.command]
for field in parser(self):
yield field
def createDescription(self):
if "meta_command" in self:
return self["meta_command"].display
else:
return self.COMMAND_DESC[self.command]
class Track(FieldSet):
def __init__(self, *args):
FieldSet.__init__(self, *args)
self._size = (8 + self["size"].value) * 8
def createFields(self):
yield String(self, "marker", 4, "Track marker (MTrk)", charset="ASCII")
yield UInt32(self, "size")
cur = None
if True:
while not self.eof:
cur = Command(self, "command[]", prev_command=cur)
yield cur
else:
size = self["size"].value
if size:
yield RawBytes(self, "raw", size)
def createDescription(self):
command = self["command[0]"]
if "meta_command" in command \
and command["meta_command"].value in (Command.META_COMMAND_TEXT, Command.META_COMMAND_NAME) \
and "text" in command:
return command["text"].value.strip("\r\n")
else:
return ""
class Header(FieldSet):
static_size = 10*8
FILE_FORMAT = {
0: "Single track",
1: "Multiple tracks, synchronous",
2: "Multiple tracks, asynchronous",
}
def createFields(self):
yield UInt32(self, "size")
yield Enum(UInt16(self, "file_format"), self.FILE_FORMAT)
yield UInt16(self, "nb_track")
yield UInt16(self, "delta_time", "Delta-time ticks per quarter note")
def createDescription(self):
return "%s; %s tracks" % (
self["file_format"].display, self["nb_track"].value)
class MidiFile(Parser):
MAGIC = "MThd"
PARSER_TAGS = {
"id": "midi",
"category": "audio",
"file_ext": ["mid", "midi"],
"mime": (u"audio/mime", ),
"magic": ((MAGIC, 0),),
"min_size": 64,
"description": "MIDI audio"
}
endian = BIG_ENDIAN
def validate(self):
if self.stream.readBytes(0, 4) != self.MAGIC:
return "Invalid signature"
if self["header/size"].value != 6:
return "Invalid header size"
return True
def createFields(self):
yield String(self, "signature", 4, r"MIDI signature (MThd)", charset="ASCII")
yield Header(self, "header")
while not self.eof:
yield Track(self, "track[]")
def createDescription(self):
return "MIDI audio: %s" % self["header"].description
def createContentSize(self):
count = self["/header/nb_track"].value - 1
start = self["track[%u]" % count].absolute_address
# Search "End of track" of last track
end = self.stream.searchBytes("\xff\x2f\x00", start, MAX_FILESIZE*8)
if end is not None:
return end + 3*8
return None
-149
View File
@@ -1,149 +0,0 @@
"""
Parser of FastTrackerII Extended Module (XM) version 1.4
Documents:
- Modplug source code (file modplug/soundlib/Load_mod.cpp)
http://sourceforge.net/projects/modplug
- Dumb source code (files include/dumb.h and src/it/readmod.c
http://dumb.sf.net/
- Documents on "MOD" format on Wotsit
http://www.wotsit.org
Compressed formats (i.e. starting with "PP20" or having "PACK" as type
are not handled. Also NoiseTracker's NST modules aren't handled, although
it might be possible: no file format and 15 samples
Author: Christophe GISQUET <christophe.gisquet@free.fr>
Creation: 18th February 2007
"""
from math import log10
from hachoir_parser import Parser
from hachoir_core.field import (FieldSet,
Bits, UInt16, UInt8,
RawBytes, String, GenericVector)
from hachoir_core.endian import BIG_ENDIAN
from hachoir_core.text_handler import textHandler
# Old NoiseTracker 15-samples modules can have anything here.
MODULE_TYPE = {
"M.K.": ("Noise/Pro-Tracker", 4),
"M!K!": ("Noise/Pro-Tracker", 4),
"M&K&": ("Noise/Pro-Tracker", 4),
"RASP": ("StarTrekker", 4),
"FLT4": ("StarTrekker", 4),
"FLT8": ("StarTrekker", 8),
"6CHN": ("FastTracker", 6),
"8CHN": ("FastTracker", 8),
"CD81": ("Octalyser", 8),
"OCTA": ("Octalyser", 8),
"FA04": ("Digital Tracker", 4),
"FA06": ("Digital Tracker", 6),
"FA08": ("Digital Tracker", 8),
}
def getFineTune(val):
return ("0", "1", "2", "3", "4", "5", "6", "7", "8",
"-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1")[val.value]
def getVolume(val):
return "%.1f dB" % (20.0*log10(val.value/64.0))
class SampleInfo(FieldSet):
static_size = 30*8
def createFields(self):
yield String(self, "name", 22, strip='\0')
yield UInt16(self, "sample_count")
yield textHandler(UInt8(self, "fine_tune"), getFineTune)
yield textHandler(UInt8(self, "volume"), getVolume)
yield UInt16(self, "loop_start", "Loop start offset in samples")
yield UInt16(self, "loop_len", "Loop length in samples")
def createValue(self):
return self["name"].value
class Header(FieldSet):
static_size = 1084*8
def createFields(self):
yield String(self, "name", 20, strip='\0')
yield GenericVector(self, "samples", 31, SampleInfo, "info")
yield UInt8(self, "length")
yield UInt8(self, "played_patterns_count")
yield GenericVector(self, "patterns", 128, UInt8, "position")
yield String(self, "type", 4)
def getNumChannels(self):
return MODULE_TYPE[self["type"].value][1]
class Note(FieldSet):
static_size = 8*4
def createFields(self):
yield Bits(self, 4, "note_hi_nibble")
yield Bits(self, 12, "period")
yield Bits(self, 4, "note_low_nibble")
yield Bits(self, 4, "effect")
yield UInt8(self, "parameter")
class Row(FieldSet):
def __init__(self, parent, name, channels, desc=None):
FieldSet.__init__(self, parent, name, description=desc)
self.channels = channels
self._size = 8*self.channels*4
def createFields(self):
for index in xrange(self.channels):
yield Note(self, "note[]")
class Pattern(FieldSet):
def __init__(self, parent, name, channels, desc=None):
FieldSet.__init__(self, parent, name, description=desc)
self.channels = channels
self._size = 64*8*self.channels*4
def createFields(self):
for index in xrange(64):
yield Row(self, "row[]", self.channels)
class AmigaModule(Parser):
PARSER_TAGS = {
"id": "mod",
"category": "audio",
"file_ext": ("mod", "nst", "wow", "oct", "sd0" ),
"mime": (u'audio/mod', u'audio/x-mod', u'audio/mod', u'audio/x-mod'),
"min_size": 1084*8,
"description": "Uncompressed amiga module"
}
endian = BIG_ENDIAN
def validate(self):
t = self.stream.readBytes(1080*8, 4)
if t not in MODULE_TYPE:
return "Invalid module type '%s'" % t
self.createValue = lambda t: "%s module, %u channels" % MODULE_TYPE[t]
return True
def createFields(self):
header = Header(self, "header")
yield header
channels = header.getNumChannels()
# Number of patterns
patterns = 0
for index in xrange(128):
patterns = max(patterns,
header["patterns/position[%u]" % index].value)
patterns += 1
# Yield patterns
for index in xrange(patterns):
yield Pattern(self, "pattern[]", channels)
# Yield samples
for index in xrange(31):
count = header["samples/info[%u]/sample_count" % index].value
if count:
self.info("Yielding sample %u: %u samples" % (index, count))
yield RawBytes(self, "sample_data[]", 2*count, \
"Sample %u" % index)

Some files were not shown because too many files have changed in this diff Show More