fileobj.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. # -*- coding: utf-8 -*-
  2. """
  3. # Simple object file format.
  4. # Copyright (c) 2011-2024 Michael Büsch <m@bues.ch>
  5. # Licensed under the GNU/GPL version 2 or later.
  6. """
  7. import errno
  8. __all__ = [
  9. "FileObjError",
  10. "FileObj",
  11. "FileObjCollection",
  12. ]
  13. class FileObjError(Exception):
  14. pass
  15. class FileObj:
  16. # Raw object layout:
  17. # [ 1 byte ] => Name length
  18. # [ x bytes ] => Name
  19. # [ 4 bytes ] => Payload data length
  20. # [ x bytes ] => Payload data
  21. __slots__ = (
  22. "__name",
  23. "__data",
  24. )
  25. def __init__(self, name, data):
  26. """Construct FileObj().
  27. name: The object name. Must be bytes-like.
  28. data: The object payload. Must be bytes-like.
  29. """
  30. assert isinstance(name, (bytes, bytearray, memoryview)),\
  31. "FileObj: Invalid 'name' type."
  32. assert isinstance(data, (bytes, bytearray, memoryview)),\
  33. "FileObj: Invalid 'data' type."
  34. self.__name = memoryview(name)
  35. self.__data = memoryview(data)
  36. if len(self.__name) > 0x7F:
  37. raise FileObjError("FileObj: Name too long")
  38. if len(self.__data) > 0x7FFFFFFF:
  39. raise FileObjError("FileObj: Data too long")
  40. def getName(self):
  41. return self.__name
  42. def getData(self):
  43. return self.__data
  44. def getRaw(self, buffer):
  45. nameLen = len(self.__name)
  46. assert nameLen <= 0x7F
  47. buffer += b"%c" % (nameLen & 0xFF)
  48. buffer += self.__name
  49. dataLen = len(self.__data)
  50. assert dataLen <= 0x7FFFFFFF
  51. buffer += b"%c" % (dataLen & 0xFF)
  52. buffer += b"%c" % ((dataLen >> 8) & 0xFF)
  53. buffer += b"%c" % ((dataLen >> 16) & 0xFF)
  54. buffer += b"%c" % ((dataLen >> 24) & 0xFF)
  55. buffer += self.__data
  56. @classmethod
  57. def parseRaw(cls, raw):
  58. assert isinstance(raw, (bytes, bytearray, memoryview)),\
  59. "FileObj: Invalid 'raw' type."
  60. raw = memoryview(raw)
  61. try:
  62. off = 0
  63. nameLen = raw[off]
  64. if nameLen & 0x80:
  65. raise FileObjError("FileObj: Name length extension bit is set, "
  66. "but not supported by this pwman version.")
  67. off += 1
  68. name = raw[off : off + nameLen]
  69. off += nameLen
  70. dataLen = (raw[off] |
  71. (raw[off + 1] << 8) |
  72. (raw[off + 2] << 16) |
  73. (raw[off + 3] << 24))
  74. if dataLen & 0x80000000:
  75. raise FileObjError("FileObj: Data length extension bit is set, "
  76. "but not supported by this pwman version.")
  77. off += 4
  78. data = raw[off : off + dataLen]
  79. off += dataLen
  80. except (IndexError, KeyError) as e:
  81. raise FileObjError("Failed to parse file object")
  82. return (cls(name, data), off)
  83. class FileObjCollection:
  84. __slots__ = (
  85. "__objects",
  86. )
  87. def __init__(self, objects):
  88. if isinstance(objects, dict):
  89. self.__objects = objects
  90. elif isinstance(objects, (list, tuple)):
  91. self.__objects = { obj.getName() : obj for obj in objects }
  92. else:
  93. assert False
  94. def writeFile(self, filepath):
  95. try:
  96. with open(filepath, "wb") as f:
  97. f.write(self.getRaw())
  98. f.flush()
  99. except IOError as e:
  100. raise FileObjError("Failed to write file: %s" % e.strerror)
  101. def getRaw(self):
  102. raw = bytearray()
  103. for obj in self.__objects.values():
  104. obj.getRaw(raw)
  105. return raw
  106. @property
  107. def objects(self):
  108. return self.__objects.values()
  109. def get(self, name, error=None, default=None):
  110. obj = self.__objects.get(name, None)
  111. if obj is None:
  112. if error:
  113. raise FileObjError(error)
  114. return default
  115. return bytes(obj.getData())
  116. @classmethod
  117. def parseRaw(cls, raw):
  118. assert isinstance(raw, (bytes, bytearray, memoryview)),\
  119. "FileObjCollection: Invalid 'raw' type."
  120. raw = memoryview(raw)
  121. offset = 0
  122. objects = {}
  123. while offset < len(raw):
  124. obj, objLen = FileObj.parseRaw(raw[offset:])
  125. objects[obj.getName()] = obj
  126. offset += objLen
  127. return cls(objects)
  128. @classmethod
  129. def parseFile(cls, filepath):
  130. try:
  131. with open(filepath, "rb") as f:
  132. rawData = f.read()
  133. except IOError as e:
  134. if e.errno != errno.ENOENT:
  135. raise FileObjError("Failed to read file: %s" % e.strerror)
  136. return None
  137. return cls.parseRaw(rawData)