From c3988f73004cbf327ea14372d5edda7404b68087 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Fri, 6 Aug 2010 11:17:57 -0700 Subject: [PATCH] safely interpret integers in packed values --- NEWS | 1 + beets/mediafile.py | 78 +++++++++++++++++++++++++----------------- test/test_mediafile.py | 26 ++++++++++++++ 3 files changed, 74 insertions(+), 31 deletions(-) diff --git a/NEWS b/NEWS index 4871d5c8c..1a166a1b0 100644 --- a/NEWS +++ b/NEWS @@ -45,6 +45,7 @@ Windows users can now just type "beet" at the prompt to run beets. * Fixed an occasional bug where Mutagen would complain that a tag was already present. +* Fixed a bug with reading invalid integers from ID3 tags. 1.0b3 ----- diff --git a/beets/mediafile.py b/beets/mediafile.py index f2ba9f131..dd5fef428 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -62,6 +62,50 @@ TYPES = { } +# Utility. + +def _safe_cast(out_type, val): + """Tries to covert val to out_type but will never raise an + exception. If the value can't be converted, then a sensible + default value is returned. out_type should be bool, int, or + unicode; otherwise, the value is just passed through. + """ + if out_type == int: + if val is None: + return 0 + elif isinstance(val, int) or isinstance(val, float): + # Just a number. + return int(val) + else: + # Process any other type as a string. + if not isinstance(val, basestring): + val = unicode(val) + # Get a number from the front of the string. + val = re.match('[0-9]*', val.strip()).group(0) + if not val: + return 0 + else: + return int(val) + + elif out_type == bool: + if val is None: + return False + else: + try: + # Should work for strings, bools, ints: + return bool(int(val)) + except ValueError: + return False + + elif out_type == unicode: + if val is None: + return u'' + else: + return unicode(val) + + else: + return val + # Flags for encoding field behavior. class Enumeration(object): @@ -146,9 +190,9 @@ class Packed(object): out = None if out is None or out == self.none_val or out == '': - return self.out_type(self.none_val) + return _safe_cast(self.out_type, self.none_val) else: - return self.out_type(out) + return _safe_cast(self.out_type, out) def __setitem__(self, index, value): @@ -336,35 +380,7 @@ class MediaField(object): if style.packing: out = Packed(out, style.packing)[style.pack_pos] - # return the appropriate type - if self.out_type == int: - if out is None: - return 0 - elif isinstance(out, int) or isinstance(out, float): - # Just a number. - return int(out) - else: - # Process any other type as a string. - if not isinstance(out, basestring): - out = unicode(out) - # Get a number from the front of the string. - out = re.match('[0-9]*', out.strip()).group(0) - if not out: - return 0 - else: - return int(out) - elif self.out_type == bool: - if out is None: - return False - else: - return bool(int(out)) # should work for strings, bools, ints - elif self.out_type == unicode: - if out is None: - return u'' - else: - return unicode(out) - else: - return out + return _safe_cast(self.out_type, out) def __set__(self, obj, val): """Set the value of this metadata field. diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 84a491b83..114ed177c 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -61,6 +61,32 @@ class EdgeTest(unittest.TestCase): self.assertEqual(f.disc, 4) self.assertEqual(f.disctotal, 5) +_sc = beets.mediafile._safe_cast +class InvalidValueToleranceTest(unittest.TestCase): + def test_packed_integer_with_extra_chars(self): + pack = beets.mediafile.Packed("06a", beets.mediafile.packing.SLASHED) + self.assertEqual(pack[0], 6) + + def test_packed_integer_invalid(self): + pack = beets.mediafile.Packed("blah", beets.mediafile.packing.SLASHED) + self.assertEqual(pack[0], 0) + + def test_packed_index_out_of_range(self): + pack = beets.mediafile.Packed("06", beets.mediafile.packing.SLASHED) + self.assertEqual(pack[1], 0) + + def test_safe_cast_string_to_int(self): + self.assertEqual(_sc(int, 'something'), 0) + + def test_safe_cast_int_string_to_int(self): + self.assertEqual(_sc(int, '20'), 20) + + def test_safe_cast_string_to_bool(self): + self.assertEqual(_sc(bool, 'whatever'), False) + + def test_safe_cast_intstring_to_bool(self): + self.assertEqual(_sc(bool, '5'), True) + class SafetyTest(unittest.TestCase): def _exccheck(self, fn, exc): fn = os.path.join('rsrc', fn)