fileobj.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. # -*- coding: utf-8 -*-
  2. """
  3. # Simple object file format.
  4. # Copyright (c) 2011-2023 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. def __init__(self, name, data):
  22. """Construct FileObj().
  23. name: The object name. Must be bytes-like.
  24. data: The object payload. Must be bytes-like.
  25. """
  26. assert isinstance(name, (bytes, bytearray)),\
  27. "FileObj: Invalid 'name' type."
  28. assert isinstance(data, (bytes, bytearray)),\
  29. "FileObj: Invalid 'data' type."
  30. if len(name) > 0x7F:
  31. raise FileObjError("FileObj: Name too long")
  32. self.__name = name
  33. if len(data) > 0x7FFFFFFF:
  34. raise FileObjError("FileObj: Data too long")
  35. self.__data = data
  36. def getName(self):
  37. return self.__name
  38. def getData(self):
  39. return self.__data
  40. def getRaw(self):
  41. r = bytearray()
  42. nameLen = len(self.__name)
  43. assert nameLen <= 0x7F
  44. r += b"%c" % (nameLen & 0xFF)
  45. r += self.__name
  46. dataLen = len(self.__data)
  47. assert dataLen <= 0x7FFFFFFF
  48. r += b"%c" % (dataLen & 0xFF)
  49. r += b"%c" % ((dataLen >> 8) & 0xFF)
  50. r += b"%c" % ((dataLen >> 16) & 0xFF)
  51. r += b"%c" % ((dataLen >> 24) & 0xFF)
  52. r += self.__data
  53. return r
  54. @classmethod
  55. def parseRaw(cls, raw):
  56. assert isinstance(raw, (bytes, bytearray)),\
  57. "FileObj: Invalid 'raw' type."
  58. try:
  59. off = 0
  60. nameLen = raw[off]
  61. if nameLen & 0x80:
  62. raise FileObjError("FileObj: Name length extension bit is set, "
  63. "but not supported by this pwman version.")
  64. off += 1
  65. name = raw[off : off + nameLen]
  66. off += nameLen
  67. dataLen = (raw[off] |
  68. (raw[off + 1] << 8) |
  69. (raw[off + 2] << 16) |
  70. (raw[off + 3] << 24))
  71. if dataLen & 0x80000000:
  72. raise FileObjError("FileObj: Data length extension bit is set, "
  73. "but not supported by this pwman version.")
  74. off += 4
  75. data = raw[off : off + dataLen]
  76. off += dataLen
  77. except (IndexError, KeyError) as e:
  78. raise FileObjError("Failed to parse file object")
  79. return (cls(name, data), off)
  80. class FileObjCollection:
  81. def __init__(self, *objects):
  82. self.objects = objects
  83. def writeFile(self, filepath):
  84. try:
  85. with open(filepath, "wb") as f:
  86. f.write(self.getRaw())
  87. f.flush()
  88. except IOError as e:
  89. raise FileObjError("Failed to write file: %s" % e.strerror)
  90. def getRaw(self):
  91. raw = bytearray()
  92. for obj in self.objects:
  93. raw += obj.getRaw()
  94. return raw
  95. def get(self, name):
  96. return [ o.getData()
  97. for o in self.objects
  98. if o.getName() == name ]
  99. def getOne(self, name, errorMsg=None, default=None):
  100. objs = self.get(name)
  101. if len(objs) != 1:
  102. if errorMsg:
  103. raise FileObjError(errorMsg)
  104. return default
  105. return objs[0]
  106. @classmethod
  107. def parseRaw(cls, raw):
  108. assert isinstance(raw, (bytes, bytearray)),\
  109. "FileObjCollection: Invalid 'raw' type."
  110. offset = 0
  111. objects = []
  112. while offset < len(raw):
  113. obj, objLen = FileObj.parseRaw(raw[offset:])
  114. objects.append(obj)
  115. offset += objLen
  116. return cls(*objects)
  117. @classmethod
  118. def parseFile(cls, filepath):
  119. try:
  120. with open(filepath, "rb") as f:
  121. rawData = f.read()
  122. except IOError as e:
  123. if e.errno != errno.ENOENT:
  124. raise FileObjError("Failed to read file: %s" % e.strerror)
  125. return None
  126. return cls.parseRaw(rawData)