rework python scripts to support Python 2 & 3

We run the unittests in both python versions to maintain coverage,
and we run things through `cros lint` during upload to help prevent
regressions.

We also switch the default to Python 3 to keep people on their toes.

BUG=chromium:982465
TEST=running unittests against python2 & python3 pass
TEST=`./fmap.py bin/example.bin` through python2 & python3 works

Change-Id: I79f0ce59664ea42dfa18e45ccde0dffc0c18227d
Reviewed-on: https://chromium-review.googlesource.com/1693888
Tested-by: Mike Frysinger <vapier@chromium.org>
Commit-Ready: Mike Frysinger <vapier@chromium.org>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Duncan Laurie <dlaurie@google.com>
diff --git a/PRESUBMIT.cfg b/PRESUBMIT.cfg
index 8238211..2150455 100644
--- a/PRESUBMIT.cfg
+++ b/PRESUBMIT.cfg
@@ -2,6 +2,8 @@
 
 [Hook Scripts]
 fmap_unittest py2 = python2 ./fmap_unittest.py
+fmap_unittest py3 = python3 ./fmap_unittest.py
+cros lint = cros lint ${PRESUBMIT_FILES}
 
 [Hook Overrides]
 cros_license_check: false
diff --git a/fmap.py b/fmap.py
index a5ec8ef..0ef9f40 100755
--- a/fmap.py
+++ b/fmap.py
@@ -1,4 +1,5 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
 #
 # Copyright 2010, Google Inc.
 # All rights reserved.
@@ -49,8 +50,10 @@
   tuple of decoded area flags.
 """
 
+from __future__ import print_function
 
 import argparse
+import copy
 import logging
 import pprint
 import struct
@@ -58,7 +61,7 @@
 
 
 # constants imported from lib/fmap.h
-FMAP_SIGNATURE = '__FMAP__'
+FMAP_SIGNATURE = b'__FMAP__'
 FMAP_VER_MAJOR = 1
 FMAP_VER_MINOR_MIN = 0
 FMAP_VER_MINOR_MAX = 1
@@ -112,7 +115,15 @@
     raise struct.error('Incompatible version')
 
   # convert null-terminated names
-  header['name'] = header['name'].strip(chr(0))
+  header['name'] = header['name'].strip(b'\x00')
+
+  # In Python 2, binary==string, so we don't need to convert.
+  if sys.version_info.major >= 3:
+    # Do the decode after verifying it to avoid decode errors due to corruption.
+    for name in FMAP_HEADER_NAMES:
+      if hasattr(header[name], 'decode'):
+        header[name] = header[name].decode('utf-8')
+
   return (header, struct.calcsize(FMAP_HEADER_FORMAT))
 
 
@@ -123,9 +134,16 @@
                            struct.unpack_from(FMAP_AREA_FORMAT, blob, offset)):
     area[name] = value
   # convert null-terminated names
-  area['name'] = area['name'].strip(chr(0))
+  area['name'] = area['name'].strip(b'\x00')
   # add a (readonly) readable FLAGS
   area['FLAGS'] = _fmap_decode_area_flags(area['flags'])
+
+  # In Python 2, binary==string, so we don't need to convert.
+  if sys.version_info.major >= 3:
+    for name in FMAP_AREA_NAMES:
+      if hasattr(area[name], 'decode'):
+        area[name] = area[name].decode('utf-8')
+
   return (area, struct.calcsize(FMAP_AREA_FORMAT))
 
 
@@ -171,7 +189,7 @@
     align *= 2
 
   while align >= FMAP_SEARCH_STRIDE:
-    for offset in xrange(align, lim + 1, align * 2):
+    for offset in range(align, lim + 1, align * 2):
       if not blob.startswith(FMAP_SIGNATURE, offset):
         continue
       try:
@@ -214,12 +232,24 @@
 
 def _fmap_encode_header(obj):
   """(internal) Encodes a FMAP header"""
+  # Convert strings to bytes.
+  obj = copy.deepcopy(obj)
+  for name in FMAP_HEADER_NAMES:
+    if hasattr(obj[name], 'encode'):
+      obj[name] = obj[name].encode('utf-8')
+
   values = [obj[name] for name in FMAP_HEADER_NAMES]
   return struct.pack(FMAP_HEADER_FORMAT, *values)
 
 
 def _fmap_encode_area(obj):
   """(internal) Encodes a FMAP area entry"""
+  # Convert strings to bytes.
+  obj = copy.deepcopy(obj)
+  for name in FMAP_AREA_NAMES:
+    if hasattr(obj[name], 'encode'):
+      obj[name] = obj[name].encode('utf-8')
+
   values = [obj[name] for name in FMAP_AREA_NAMES]
   return struct.pack(FMAP_AREA_FORMAT, *values)
 
diff --git a/fmap_unittest.py b/fmap_unittest.py
index 4a5bffa..91b346b 100755
--- a/fmap_unittest.py
+++ b/fmap_unittest.py
@@ -1,10 +1,14 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
 #
 # Copyright 2017 The Chromium OS Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
+
 """Unit test for fmap module."""
 
+from __future__ import print_function
+
 import struct
 import unittest
 
@@ -54,7 +58,7 @@
   maxDiff = None
 
   def setUp(self):
-    with open('bin/example.bin') as f:
+    with open('bin/example.bin', 'rb') as f:
       self.example_blob = f.read()
 
   def testDecode(self):