gen_esp32part.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. #!/usr/bin/env python
  2. #
  3. # ESP32 partition table generation tool
  4. #
  5. # Converts partition tables to/from CSV and binary formats.
  6. #
  7. # See https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/partition-tables.html
  8. # for explanation of partition table structure and uses.
  9. #
  10. # SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD
  11. # SPDX-License-Identifier: Apache-2.0
  12. from __future__ import division, print_function, unicode_literals
  13. import argparse
  14. import binascii
  15. import errno
  16. import hashlib
  17. import os
  18. import re
  19. import struct
  20. import sys
  21. MAX_PARTITION_LENGTH = 0xC00 # 3K for partition data (96 entries) leaves 1K in a 4K sector for signature
  22. MD5_PARTITION_BEGIN = b'\xEB\xEB' + b'\xFF' * 14 # The first 2 bytes are like magic numbers for MD5 sum
  23. PARTITION_TABLE_SIZE = 0x1000 # Size of partition table
  24. MIN_PARTITION_SUBTYPE_APP_OTA = 0x10
  25. NUM_PARTITION_SUBTYPE_APP_OTA = 16
  26. SECURE_NONE = None
  27. SECURE_V1 = 'v1'
  28. SECURE_V2 = 'v2'
  29. __version__ = '1.3'
  30. APP_TYPE = 0x00
  31. DATA_TYPE = 0x01
  32. TYPES = {
  33. 'app': APP_TYPE,
  34. 'data': DATA_TYPE,
  35. }
  36. def get_ptype_as_int(ptype):
  37. """ Convert a string which might be numeric or the name of a partition type to an integer """
  38. try:
  39. return TYPES[ptype]
  40. except KeyError:
  41. try:
  42. return int(ptype, 0)
  43. except TypeError:
  44. return ptype
  45. # Keep this map in sync with esp_partition_subtype_t enum in esp_partition.h
  46. SUBTYPES = {
  47. APP_TYPE: {
  48. 'factory': 0x00,
  49. 'test': 0x20,
  50. },
  51. DATA_TYPE: {
  52. 'ota': 0x00,
  53. 'phy': 0x01,
  54. 'nvs': 0x02,
  55. 'coredump': 0x03,
  56. 'nvs_keys': 0x04,
  57. 'efuse': 0x05,
  58. 'undefined': 0x06,
  59. 'esphttpd': 0x80,
  60. 'fat': 0x81,
  61. 'spiffs': 0x82,
  62. 'littlefs': 0x83,
  63. },
  64. }
  65. def get_subtype_as_int(ptype, subtype):
  66. """ Convert a string which might be numeric or the name of a partition subtype to an integer """
  67. try:
  68. return SUBTYPES[get_ptype_as_int(ptype)][subtype]
  69. except KeyError:
  70. try:
  71. return int(subtype, 0)
  72. except TypeError:
  73. return subtype
  74. ALIGNMENT = {
  75. APP_TYPE: 0x10000,
  76. DATA_TYPE: 0x1000,
  77. }
  78. def get_alignment_offset_for_type(ptype):
  79. return ALIGNMENT.get(ptype, ALIGNMENT[DATA_TYPE])
  80. def get_alignment_size_for_type(ptype):
  81. if ptype == APP_TYPE and secure == SECURE_V1:
  82. # For secure boot v1 case, app partition must be 64K aligned
  83. # signature block (68 bytes) lies at the very end of 64K block
  84. return 0x10000
  85. if ptype == APP_TYPE and secure == SECURE_V2:
  86. # For secure boot v2 case, app partition must be 4K aligned
  87. # signature block (4K) is kept after padding the unsigned image to 64K boundary
  88. return 0x1000
  89. # No specific size alignement requirement as such
  90. return 0x1
  91. def get_partition_type(ptype):
  92. if ptype == 'app':
  93. return APP_TYPE
  94. if ptype == 'data':
  95. return DATA_TYPE
  96. raise InputError('Invalid partition type')
  97. def add_extra_subtypes(csv):
  98. for line_no in csv:
  99. try:
  100. fields = [line.strip() for line in line_no.split(',')]
  101. for subtype, subtype_values in SUBTYPES.items():
  102. if (int(fields[2], 16) in subtype_values.values() and subtype == get_partition_type(fields[0])):
  103. raise ValueError('Found duplicate value in partition subtype')
  104. SUBTYPES[TYPES[fields[0]]][fields[1]] = int(fields[2], 16)
  105. except InputError as err:
  106. raise InputError('Error parsing custom subtypes: %s' % err)
  107. quiet = False
  108. md5sum = True
  109. secure = SECURE_NONE
  110. offset_part_table = 0
  111. def status(msg):
  112. """ Print status message to stderr """
  113. if not quiet:
  114. critical(msg)
  115. def critical(msg):
  116. """ Print critical message to stderr """
  117. sys.stderr.write(msg)
  118. sys.stderr.write('\n')
  119. class PartitionTable(list):
  120. def __init__(self):
  121. super(PartitionTable, self).__init__(self)
  122. @classmethod
  123. def from_file(cls, f):
  124. data = f.read()
  125. data_is_binary = data[0:2] == PartitionDefinition.MAGIC_BYTES
  126. if data_is_binary:
  127. status('Parsing binary partition input...')
  128. return cls.from_binary(data), True
  129. data = data.decode()
  130. status('Parsing CSV input...')
  131. return cls.from_csv(data), False
  132. @classmethod
  133. def from_csv(cls, csv_contents):
  134. res = PartitionTable()
  135. lines = csv_contents.splitlines()
  136. def expand_vars(f):
  137. f = os.path.expandvars(f)
  138. m = re.match(r'(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)', f)
  139. if m:
  140. raise InputError("unknown variable '%s'" % m.group(1))
  141. return f
  142. for line_no in range(len(lines)):
  143. line = expand_vars(lines[line_no]).strip()
  144. if line.startswith('#') or len(line) == 0:
  145. continue
  146. try:
  147. res.append(PartitionDefinition.from_csv(line, line_no + 1))
  148. except InputError as err:
  149. raise InputError('Error at line %d: %s\nPlease check extra_partition_subtypes.inc file in build/config directory' % (line_no + 1, err))
  150. except Exception:
  151. critical('Unexpected error parsing CSV line %d: %s' % (line_no + 1, line))
  152. raise
  153. # fix up missing offsets & negative sizes
  154. last_end = offset_part_table + PARTITION_TABLE_SIZE # first offset after partition table
  155. for e in res:
  156. if e.offset is not None and e.offset < last_end:
  157. if e == res[0]:
  158. raise InputError('CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. '
  159. 'But partition table occupies the whole sector 0x%x. '
  160. 'Use a free offset 0x%x or higher.'
  161. % (e.line_no, e.offset, offset_part_table, last_end))
  162. else:
  163. raise InputError('CSV Error at line %d: Partitions overlap. Partition sets offset 0x%x. Previous partition ends 0x%x'
  164. % (e.line_no, e.offset, last_end))
  165. if e.offset is None:
  166. pad_to = get_alignment_offset_for_type(e.type)
  167. if last_end % pad_to != 0:
  168. last_end += pad_to - (last_end % pad_to)
  169. e.offset = last_end
  170. if e.size < 0:
  171. e.size = -e.size - e.offset
  172. last_end = e.offset + e.size
  173. return res
  174. def __getitem__(self, item):
  175. """ Allow partition table access via name as well as by
  176. numeric index. """
  177. if isinstance(item, str):
  178. for x in self:
  179. if x.name == item:
  180. return x
  181. raise ValueError("No partition entry named '%s'" % item)
  182. else:
  183. return super(PartitionTable, self).__getitem__(item)
  184. def find_by_type(self, ptype, subtype):
  185. """ Return a partition by type & subtype, returns
  186. None if not found """
  187. # convert ptype & subtypes names (if supplied this way) to integer values
  188. ptype = get_ptype_as_int(ptype)
  189. subtype = get_subtype_as_int(ptype, subtype)
  190. for p in self:
  191. if p.type == ptype and p.subtype == subtype:
  192. yield p
  193. return
  194. def find_by_name(self, name):
  195. for p in self:
  196. if p.name == name:
  197. return p
  198. return None
  199. def verify(self):
  200. # verify each partition individually
  201. for p in self:
  202. p.verify()
  203. # check on duplicate name
  204. names = [p.name for p in self]
  205. duplicates = set(n for n in names if names.count(n) > 1)
  206. # print sorted duplicate partitions by name
  207. if len(duplicates) != 0:
  208. critical('A list of partitions that have the same name:')
  209. for p in sorted(self, key=lambda x:x.name):
  210. if len(duplicates.intersection([p.name])) != 0:
  211. critical('%s' % (p.to_csv()))
  212. raise InputError('Partition names must be unique')
  213. # check for overlaps
  214. last = None
  215. for p in sorted(self, key=lambda x:x.offset):
  216. if p.offset < offset_part_table + PARTITION_TABLE_SIZE:
  217. raise InputError('Partition offset 0x%x is below 0x%x' % (p.offset, offset_part_table + PARTITION_TABLE_SIZE))
  218. if last is not None and p.offset < last.offset + last.size:
  219. raise InputError('Partition at 0x%x overlaps 0x%x-0x%x' % (p.offset, last.offset, last.offset + last.size - 1))
  220. last = p
  221. # check that otadata should be unique
  222. otadata_duplicates = [p for p in self if p.type == TYPES['data'] and p.subtype == SUBTYPES[DATA_TYPE]['ota']]
  223. if len(otadata_duplicates) > 1:
  224. for p in otadata_duplicates:
  225. critical('%s' % (p.to_csv()))
  226. raise InputError('Found multiple otadata partitions. Only one partition can be defined with type="data"(1) and subtype="ota"(0).')
  227. if len(otadata_duplicates) == 1 and otadata_duplicates[0].size != 0x2000:
  228. p = otadata_duplicates[0]
  229. critical('%s' % (p.to_csv()))
  230. raise InputError('otadata partition must have size = 0x2000')
  231. def flash_size(self):
  232. """ Return the size that partitions will occupy in flash
  233. (ie the offset the last partition ends at)
  234. """
  235. try:
  236. last = sorted(self, reverse=True)[0]
  237. except IndexError:
  238. return 0 # empty table!
  239. return last.offset + last.size
  240. def verify_size_fits(self, flash_size_bytes: int) -> None:
  241. """ Check that partition table fits into the given flash size.
  242. Raises InputError otherwise.
  243. """
  244. table_size = self.flash_size()
  245. if flash_size_bytes < table_size:
  246. mb = 1024 * 1024
  247. raise InputError('Partitions tables occupies %.1fMB of flash (%d bytes) which does not fit in configured '
  248. "flash size %dMB. Change the flash size in menuconfig under the 'Serial Flasher Config' menu." %
  249. (table_size / mb, table_size, flash_size_bytes / mb))
  250. @classmethod
  251. def from_binary(cls, b):
  252. md5 = hashlib.md5()
  253. result = cls()
  254. for o in range(0,len(b),32):
  255. data = b[o:o + 32]
  256. if len(data) != 32:
  257. raise InputError('Partition table length must be a multiple of 32 bytes')
  258. if data == b'\xFF' * 32:
  259. return result # got end marker
  260. if md5sum and data[:2] == MD5_PARTITION_BEGIN[:2]: # check only the magic number part
  261. if data[16:] == md5.digest():
  262. continue # the next iteration will check for the end marker
  263. else:
  264. raise InputError("MD5 checksums don't match! (computed: 0x%s, parsed: 0x%s)" % (md5.hexdigest(), binascii.hexlify(data[16:])))
  265. else:
  266. md5.update(data)
  267. result.append(PartitionDefinition.from_binary(data))
  268. raise InputError('Partition table is missing an end-of-table marker')
  269. def to_binary(self):
  270. result = b''.join(e.to_binary() for e in self)
  271. if md5sum:
  272. result += MD5_PARTITION_BEGIN + hashlib.md5(result).digest()
  273. if len(result) >= MAX_PARTITION_LENGTH:
  274. raise InputError('Binary partition table length (%d) longer than max' % len(result))
  275. result += b'\xFF' * (MAX_PARTITION_LENGTH - len(result)) # pad the sector, for signing
  276. return result
  277. def to_csv(self, simple_formatting=False):
  278. rows = ['# ESP-IDF Partition Table',
  279. '# Name, Type, SubType, Offset, Size, Flags']
  280. rows += [x.to_csv(simple_formatting) for x in self]
  281. return '\n'.join(rows) + '\n'
  282. class PartitionDefinition(object):
  283. MAGIC_BYTES = b'\xAA\x50'
  284. # dictionary maps flag name (as used in CSV flags list, property name)
  285. # to bit set in flags words in binary format
  286. FLAGS = {
  287. 'encrypted': 0,
  288. 'readonly': 1
  289. }
  290. # add subtypes for the 16 OTA slot values ("ota_XX, etc.")
  291. for ota_slot in range(NUM_PARTITION_SUBTYPE_APP_OTA):
  292. SUBTYPES[TYPES['app']]['ota_%d' % ota_slot] = MIN_PARTITION_SUBTYPE_APP_OTA + ota_slot
  293. def __init__(self):
  294. self.name = ''
  295. self.type = None
  296. self.subtype = None
  297. self.offset = None
  298. self.size = None
  299. self.encrypted = False
  300. self.readonly = False
  301. @classmethod
  302. def from_csv(cls, line, line_no):
  303. """ Parse a line from the CSV """
  304. line_w_defaults = line + ',,,,' # lazy way to support default fields
  305. fields = [f.strip() for f in line_w_defaults.split(',')]
  306. res = PartitionDefinition()
  307. res.line_no = line_no
  308. res.name = fields[0]
  309. res.type = res.parse_type(fields[1])
  310. res.subtype = res.parse_subtype(fields[2])
  311. res.offset = res.parse_address(fields[3])
  312. res.size = res.parse_address(fields[4])
  313. if res.size is None:
  314. raise InputError("Size field can't be empty")
  315. flags = fields[5].split(':')
  316. for flag in flags:
  317. if flag in cls.FLAGS:
  318. setattr(res, flag, True)
  319. elif len(flag) > 0:
  320. raise InputError("CSV flag column contains unknown flag '%s'" % (flag))
  321. return res
  322. def __eq__(self, other):
  323. return self.name == other.name and self.type == other.type \
  324. and self.subtype == other.subtype and self.offset == other.offset \
  325. and self.size == other.size
  326. def __repr__(self):
  327. def maybe_hex(x):
  328. return '0x%x' % x if x is not None else 'None'
  329. return "PartitionDefinition('%s', 0x%x, 0x%x, %s, %s)" % (self.name, self.type, self.subtype or 0,
  330. maybe_hex(self.offset), maybe_hex(self.size))
  331. def __str__(self):
  332. return "Part '%s' %d/%d @ 0x%x size 0x%x" % (self.name, self.type, self.subtype, self.offset or -1, self.size or -1)
  333. def __cmp__(self, other):
  334. return self.offset - other.offset
  335. def __lt__(self, other):
  336. return self.offset < other.offset
  337. def __gt__(self, other):
  338. return self.offset > other.offset
  339. def __le__(self, other):
  340. return self.offset <= other.offset
  341. def __ge__(self, other):
  342. return self.offset >= other.offset
  343. def parse_type(self, strval):
  344. if strval == '':
  345. raise InputError("Field 'type' can't be left empty.")
  346. return parse_int(strval, TYPES)
  347. def parse_subtype(self, strval):
  348. if strval == '':
  349. if self.type == TYPES['app']:
  350. raise InputError('App partition cannot have an empty subtype')
  351. return SUBTYPES[DATA_TYPE]['undefined']
  352. return parse_int(strval, SUBTYPES.get(self.type, {}))
  353. def parse_address(self, strval):
  354. if strval == '':
  355. return None # PartitionTable will fill in default
  356. return parse_int(strval)
  357. def verify(self):
  358. if self.type is None:
  359. raise ValidationError(self, 'Type field is not set')
  360. if self.subtype is None:
  361. raise ValidationError(self, 'Subtype field is not set')
  362. if self.offset is None:
  363. raise ValidationError(self, 'Offset field is not set')
  364. if self.size is None:
  365. raise ValidationError(self, 'Size field is not set')
  366. offset_align = get_alignment_offset_for_type(self.type)
  367. if self.offset % offset_align:
  368. raise ValidationError(self, 'Offset 0x%x is not aligned to 0x%x' % (self.offset, offset_align))
  369. if self.type == APP_TYPE and secure is not SECURE_NONE:
  370. size_align = get_alignment_size_for_type(self.type)
  371. if self.size % size_align:
  372. raise ValidationError(self, 'Size 0x%x is not aligned to 0x%x' % (self.size, size_align))
  373. if self.name in TYPES and TYPES.get(self.name, '') != self.type:
  374. critical("WARNING: Partition has name '%s' which is a partition type, but does not match this partition's "
  375. 'type (0x%x). Mistake in partition table?' % (self.name, self.type))
  376. all_subtype_names = []
  377. for names in (t.keys() for t in SUBTYPES.values()):
  378. all_subtype_names += names
  379. if self.name in all_subtype_names and SUBTYPES.get(self.type, {}).get(self.name, '') != self.subtype:
  380. critical("WARNING: Partition has name '%s' which is a partition subtype, but this partition has "
  381. 'non-matching type 0x%x and subtype 0x%x. Mistake in partition table?' % (self.name, self.type, self.subtype))
  382. always_rw_data_subtypes = [SUBTYPES[DATA_TYPE]['ota'], SUBTYPES[DATA_TYPE]['coredump']]
  383. if self.type == TYPES['data'] and self.subtype in always_rw_data_subtypes and self.readonly is True:
  384. raise ValidationError(self, "'%s' partition of type %s and subtype %s is always read-write and cannot be read-only" %
  385. (self.name, self.type, self.subtype))
  386. STRUCT_FORMAT = b'<2sBBLL16sL'
  387. @classmethod
  388. def from_binary(cls, b):
  389. if len(b) != 32:
  390. raise InputError('Partition definition length must be exactly 32 bytes. Got %d bytes.' % len(b))
  391. res = cls()
  392. (magic, res.type, res.subtype, res.offset,
  393. res.size, res.name, flags) = struct.unpack(cls.STRUCT_FORMAT, b)
  394. if b'\x00' in res.name: # strip null byte padding from name string
  395. res.name = res.name[:res.name.index(b'\x00')]
  396. res.name = res.name.decode()
  397. if magic != cls.MAGIC_BYTES:
  398. raise InputError('Invalid magic bytes (%r) for partition definition' % magic)
  399. for flag,bit in cls.FLAGS.items():
  400. if flags & (1 << bit):
  401. setattr(res, flag, True)
  402. flags &= ~(1 << bit)
  403. if flags != 0:
  404. critical('WARNING: Partition definition had unknown flag(s) 0x%08x. Newer binary format?' % flags)
  405. return res
  406. def get_flags_list(self):
  407. return [flag for flag in self.FLAGS.keys() if getattr(self, flag)]
  408. def to_binary(self):
  409. flags = sum((1 << self.FLAGS[flag]) for flag in self.get_flags_list())
  410. return struct.pack(self.STRUCT_FORMAT,
  411. self.MAGIC_BYTES,
  412. self.type, self.subtype,
  413. self.offset, self.size,
  414. self.name.encode(),
  415. flags)
  416. def to_csv(self, simple_formatting=False):
  417. def addr_format(a, include_sizes):
  418. if not simple_formatting and include_sizes:
  419. for (val, suffix) in [(0x100000, 'M'), (0x400, 'K')]:
  420. if a % val == 0:
  421. return '%d%s' % (a // val, suffix)
  422. return '0x%x' % a
  423. def lookup_keyword(t, keywords):
  424. for k,v in keywords.items():
  425. if simple_formatting is False and t == v:
  426. return k
  427. return '%d' % t
  428. def generate_text_flags():
  429. """ colon-delimited list of flags """
  430. return ':'.join(self.get_flags_list())
  431. return ','.join([self.name,
  432. lookup_keyword(self.type, TYPES),
  433. lookup_keyword(self.subtype, SUBTYPES.get(self.type, {})),
  434. addr_format(self.offset, False),
  435. addr_format(self.size, True),
  436. generate_text_flags()])
  437. def parse_int(v, keywords={}):
  438. """Generic parser for integer fields - int(x,0) with provision for
  439. k/m/K/M suffixes and 'keyword' value lookup.
  440. """
  441. try:
  442. for letter, multiplier in [('k', 1024), ('m', 1024 * 1024)]:
  443. if v.lower().endswith(letter):
  444. return parse_int(v[:-1], keywords) * multiplier
  445. return int(v, 0)
  446. except ValueError:
  447. if len(keywords) == 0:
  448. raise InputError('Invalid field value %s' % v)
  449. try:
  450. return keywords[v.lower()]
  451. except KeyError:
  452. raise InputError("Value '%s' is not valid. Known keywords: %s" % (v, ', '.join(keywords)))
  453. def main():
  454. global quiet
  455. global md5sum
  456. global offset_part_table
  457. global secure
  458. parser = argparse.ArgumentParser(description='ESP32 partition table utility')
  459. parser.add_argument('--flash-size', help='Optional flash size limit, checks partition table fits in flash',
  460. nargs='?', choices=['1MB', '2MB', '4MB', '8MB', '16MB', '32MB', '64MB', '128MB'])
  461. parser.add_argument('--disable-md5sum', help='Disable md5 checksum for the partition table', default=False, action='store_true')
  462. parser.add_argument('--no-verify', help="Don't verify partition table fields", action='store_true')
  463. parser.add_argument('--verify', '-v', help='Verify partition table fields (deprecated, this behaviour is '
  464. 'enabled by default and this flag does nothing.', action='store_true')
  465. parser.add_argument('--quiet', '-q', help="Don't print non-critical status messages to stderr", action='store_true')
  466. parser.add_argument('--offset', '-o', help='Set offset partition table', default='0x8000')
  467. parser.add_argument('--secure', help='Require app partitions to be suitable for secure boot', nargs='?', const=SECURE_V1, choices=[SECURE_V1, SECURE_V2])
  468. parser.add_argument('--extra-partition-subtypes', help='Extra partition subtype entries', nargs='*')
  469. parser.add_argument('input', help='Path to CSV or binary file to parse.', type=argparse.FileType('rb'))
  470. parser.add_argument('output', help='Path to output converted binary or CSV file. Will use stdout if omitted.',
  471. nargs='?', default='-')
  472. args = parser.parse_args()
  473. quiet = args.quiet
  474. md5sum = not args.disable_md5sum
  475. secure = args.secure
  476. offset_part_table = int(args.offset, 0)
  477. if args.extra_partition_subtypes:
  478. add_extra_subtypes(args.extra_partition_subtypes)
  479. table, input_is_binary = PartitionTable.from_file(args.input)
  480. if not args.no_verify:
  481. status('Verifying table...')
  482. table.verify()
  483. if args.flash_size:
  484. size_mb = int(args.flash_size.replace('MB', ''))
  485. table.verify_size_fits(size_mb * 1024 * 1024)
  486. # Make sure that the output directory is created
  487. output_dir = os.path.abspath(os.path.dirname(args.output))
  488. if not os.path.exists(output_dir):
  489. try:
  490. os.makedirs(output_dir)
  491. except OSError as exc:
  492. if exc.errno != errno.EEXIST:
  493. raise
  494. if input_is_binary:
  495. output = table.to_csv()
  496. with sys.stdout if args.output == '-' else open(args.output, 'w') as f:
  497. f.write(output)
  498. else:
  499. output = table.to_binary()
  500. try:
  501. stdout_binary = sys.stdout.buffer # Python 3
  502. except AttributeError:
  503. stdout_binary = sys.stdout
  504. with stdout_binary if args.output == '-' else open(args.output, 'wb') as f:
  505. f.write(output)
  506. class InputError(RuntimeError):
  507. def __init__(self, e):
  508. super(InputError, self).__init__(e)
  509. class ValidationError(InputError):
  510. def __init__(self, partition, message):
  511. super(ValidationError, self).__init__(
  512. 'Partition %s invalid: %s' % (partition.name, message))
  513. if __name__ == '__main__':
  514. try:
  515. main()
  516. except InputError as e:
  517. print(e, file=sys.stderr)
  518. sys.exit(2)