mirror of
				https://xff.cz/git/u-boot/
				synced 2025-10-31 18:35:42 +01:00 
			
		
		
		
	binman: Add an 'extract' command
It is useful to be able to extract all binaries from the image, or a subset of them. Add a new 'extract' command to handle this. Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
		| @@ -533,6 +533,30 @@ or with wildcards: | |||||||
|       image-header          bf8     8  image-header     bf8 |       image-header          bf8     8  image-header     bf8 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Extracting files from images | ||||||
|  | ---------------------------- | ||||||
|  |  | ||||||
|  | You can extract files from an existing firmware image created by binman, | ||||||
|  | provided that there is an 'fdtmap' entry in the image. For example: | ||||||
|  |  | ||||||
|  |     $ binman extract -i image.bin section/cbfs/u-boot | ||||||
|  |  | ||||||
|  | which will write the uncompressed contents of that entry to the file 'u-boot' in | ||||||
|  | the current directory. You can also extract to a particular file, in this case | ||||||
|  | u-boot.bin: | ||||||
|  |  | ||||||
|  |     $ binman extract -i image.bin section/cbfs/u-boot -f u-boot.bin | ||||||
|  |  | ||||||
|  | It is possible to extract all files into a destination directory, which will | ||||||
|  | put files in subdirectories matching the entry hierarchy: | ||||||
|  |  | ||||||
|  |     $ binman extract -i image.bin -O outdir | ||||||
|  |  | ||||||
|  | or just a selection: | ||||||
|  |  | ||||||
|  |     $ binman extract -i image.bin "*u-boot*" -O outdir | ||||||
|  |  | ||||||
|  |  | ||||||
| Logging | Logging | ||||||
| ------- | ------- | ||||||
|  |  | ||||||
| @@ -883,7 +907,6 @@ Some ideas: | |||||||
| - Use of-platdata to make the information available to code that is unable | - Use of-platdata to make the information available to code that is unable | ||||||
|   to use device tree (such as a very small SPL image) |   to use device tree (such as a very small SPL image) | ||||||
| - Allow easy building of images by specifying just the board name | - Allow easy building of images by specifying just the board name | ||||||
| - Add an option to decode an image into the constituent binaries |  | ||||||
| - Support building an image for a board (-b) more completely, with a | - Support building an image for a board (-b) more completely, with a | ||||||
|   configurable build directory |   configurable build directory | ||||||
| - Support updating binaries in an image (with no size change / repacking) | - Support updating binaries in an image (with no size change / repacking) | ||||||
|   | |||||||
| @@ -71,6 +71,19 @@ controlled by a description in the board device tree.''' | |||||||
|     list_parser.add_argument('paths', type=str, nargs='*', |     list_parser.add_argument('paths', type=str, nargs='*', | ||||||
|                              help='Paths within file to list (wildcard)') |                              help='Paths within file to list (wildcard)') | ||||||
|  |  | ||||||
|  |     extract_parser = subparsers.add_parser('extract', | ||||||
|  |                                            help='Extract files from an image') | ||||||
|  |     extract_parser.add_argument('-i', '--image', type=str, required=True, | ||||||
|  |                                 help='Image filename to extract') | ||||||
|  |     extract_parser.add_argument('-f', '--filename', type=str, | ||||||
|  |                                 help='Output filename to write to') | ||||||
|  |     extract_parser.add_argument('-O', '--outdir', type=str, default='', | ||||||
|  |         help='Path to directory to use for output files') | ||||||
|  |     extract_parser.add_argument('paths', type=str, nargs='*', | ||||||
|  |                                 help='Paths within file to extract (wildcard)') | ||||||
|  |     extract_parser.add_argument('-U', '--uncompressed', action='store_true', | ||||||
|  |         help='Output raw uncompressed data for compressed entries') | ||||||
|  |  | ||||||
|     test_parser = subparsers.add_parser('test', help='Run tests') |     test_parser = subparsers.add_parser('test', help='Run tests') | ||||||
|     test_parser.add_argument('-P', '--processes', type=int, |     test_parser.add_argument('-P', '--processes', type=int, | ||||||
|         help='set number of processes to use for running tests') |         help='set number of processes to use for running tests') | ||||||
|   | |||||||
| @@ -118,6 +118,57 @@ def ReadEntry(image_fname, entry_path, decomp=True): | |||||||
|     return entry.ReadData(decomp) |     return entry.ReadData(decomp) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def ExtractEntries(image_fname, output_fname, outdir, entry_paths, | ||||||
|  |                    decomp=True): | ||||||
|  |     """Extract the data from one or more entries and write it to files | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         image_fname: Image filename to process | ||||||
|  |         output_fname: Single output filename to use if extracting one file, None | ||||||
|  |             otherwise | ||||||
|  |         outdir: Output directory to use (for any number of files), else None | ||||||
|  |         entry_paths: List of entry paths to extract | ||||||
|  |         decomp: True to compress the entry data | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         List of EntryInfo records that were written | ||||||
|  |     """ | ||||||
|  |     image = Image.FromFile(image_fname) | ||||||
|  |  | ||||||
|  |     # Output an entry to a single file, as a special case | ||||||
|  |     if output_fname: | ||||||
|  |         if not entry_paths: | ||||||
|  |             raise ValueError('Must specify an entry path to write with -o') | ||||||
|  |         if len(entry_paths) != 1: | ||||||
|  |             raise ValueError('Must specify exactly one entry path to write with -o') | ||||||
|  |         entry = image.FindEntryPath(entry_paths[0]) | ||||||
|  |         data = entry.ReadData(decomp) | ||||||
|  |         tools.WriteFile(output_fname, data) | ||||||
|  |         tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname)) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     # Otherwise we will output to a path given by the entry path of each entry. | ||||||
|  |     # This means that entries will appear in subdirectories if they are part of | ||||||
|  |     # a sub-section. | ||||||
|  |     einfos = image.GetListEntries(entry_paths)[0] | ||||||
|  |     tout.Notice('%d entries match and will be written' % len(einfos)) | ||||||
|  |     for einfo in einfos: | ||||||
|  |         entry = einfo.entry | ||||||
|  |         data = entry.ReadData(decomp) | ||||||
|  |         path = entry.GetPath()[1:] | ||||||
|  |         fname = os.path.join(outdir, path) | ||||||
|  |  | ||||||
|  |         # If this entry has children, create a directory for it and put its | ||||||
|  |         # data in a file called 'root' in that directory | ||||||
|  |         if entry.GetEntries(): | ||||||
|  |             if not os.path.exists(fname): | ||||||
|  |                 os.makedirs(fname) | ||||||
|  |             fname = os.path.join(fname, 'root') | ||||||
|  |         tout.Notice("Write entry '%s' to '%s'" % (entry.GetPath(), fname)) | ||||||
|  |         tools.WriteFile(fname, data) | ||||||
|  |     return einfos | ||||||
|  |  | ||||||
|  |  | ||||||
| def Binman(args): | def Binman(args): | ||||||
|     """The main control code for binman |     """The main control code for binman | ||||||
|  |  | ||||||
| @@ -142,6 +193,15 @@ def Binman(args): | |||||||
|         ListEntries(args.image, args.paths) |         ListEntries(args.image, args.paths) | ||||||
|         return 0 |         return 0 | ||||||
|  |  | ||||||
|  |     if args.cmd == 'extract': | ||||||
|  |         try: | ||||||
|  |             tools.PrepareOutputDir(None) | ||||||
|  |             ExtractEntries(args.image, args.filename, args.outdir, args.paths, | ||||||
|  |                            not args.uncompressed) | ||||||
|  |         finally: | ||||||
|  |             tools.FinaliseOutputDir() | ||||||
|  |         return 0 | ||||||
|  |  | ||||||
|     # Try to figure out which device tree contains our image description |     # Try to figure out which device tree contains our image description | ||||||
|     if args.dt: |     if args.dt: | ||||||
|         dtb_fname = args.dt |         dtb_fname = args.dt | ||||||
|   | |||||||
| @@ -2446,6 +2446,43 @@ class TestFunctional(unittest.TestCase): | |||||||
|         data = self._RunExtractCmd('u-boot') |         data = self._RunExtractCmd('u-boot') | ||||||
|         self.assertEqual(U_BOOT_DATA, data) |         self.assertEqual(U_BOOT_DATA, data) | ||||||
|  |  | ||||||
|  |     def testExtractSection(self): | ||||||
|  |         """Test extracting the files in a section""" | ||||||
|  |         data = self._RunExtractCmd('section') | ||||||
|  |         cbfs_data = data[:0x400] | ||||||
|  |         cbfs = cbfs_util.CbfsReader(cbfs_data) | ||||||
|  |         self.assertEqual(['u-boot', 'u-boot-dtb', ''], cbfs.files.keys()) | ||||||
|  |         dtb_data = data[0x400:] | ||||||
|  |         dtb = self._decompress(dtb_data) | ||||||
|  |         self.assertEqual(EXTRACT_DTB_SIZE, len(dtb)) | ||||||
|  |  | ||||||
|  |     def testExtractCompressed(self): | ||||||
|  |         """Test extracting compressed data""" | ||||||
|  |         data = self._RunExtractCmd('section/u-boot-dtb') | ||||||
|  |         self.assertEqual(EXTRACT_DTB_SIZE, len(data)) | ||||||
|  |  | ||||||
|  |     def testExtractRaw(self): | ||||||
|  |         """Test extracting compressed data without decompressing it""" | ||||||
|  |         data = self._RunExtractCmd('section/u-boot-dtb', decomp=False) | ||||||
|  |         dtb = self._decompress(data) | ||||||
|  |         self.assertEqual(EXTRACT_DTB_SIZE, len(dtb)) | ||||||
|  |  | ||||||
|  |     def testExtractCbfs(self): | ||||||
|  |         """Test extracting CBFS data""" | ||||||
|  |         data = self._RunExtractCmd('section/cbfs/u-boot') | ||||||
|  |         self.assertEqual(U_BOOT_DATA, data) | ||||||
|  |  | ||||||
|  |     def testExtractCbfsCompressed(self): | ||||||
|  |         """Test extracting CBFS compressed data""" | ||||||
|  |         data = self._RunExtractCmd('section/cbfs/u-boot-dtb') | ||||||
|  |         self.assertEqual(EXTRACT_DTB_SIZE, len(data)) | ||||||
|  |  | ||||||
|  |     def testExtractCbfsRaw(self): | ||||||
|  |         """Test extracting CBFS compressed data without decompressing it""" | ||||||
|  |         data = self._RunExtractCmd('section/cbfs/u-boot-dtb', decomp=False) | ||||||
|  |         dtb = tools.Decompress(data, 'lzma') | ||||||
|  |         self.assertEqual(EXTRACT_DTB_SIZE, len(dtb)) | ||||||
|  |  | ||||||
|     def testExtractBadEntry(self): |     def testExtractBadEntry(self): | ||||||
|         """Test extracting a bad section path""" |         """Test extracting a bad section path""" | ||||||
|         with self.assertRaises(ValueError) as e: |         with self.assertRaises(ValueError) as e: | ||||||
| @@ -2465,6 +2502,158 @@ class TestFunctional(unittest.TestCase): | |||||||
|         with self.assertRaises(ValueError) as e: |         with self.assertRaises(ValueError) as e: | ||||||
|             control.ReadEntry(fname, 'name') |             control.ReadEntry(fname, 'name') | ||||||
|  |  | ||||||
|  |     def testExtractCmd(self): | ||||||
|  |         """Test extracting a file fron an image on the command line""" | ||||||
|  |         self._CheckLz4() | ||||||
|  |         self._DoReadFileRealDtb('130_list_fdtmap.dts') | ||||||
|  |         image_fname = tools.GetOutputFilename('image.bin') | ||||||
|  |         fname = os.path.join(self._indir, 'output.extact') | ||||||
|  |         with test_util.capture_sys_output() as (stdout, stderr): | ||||||
|  |             self._DoBinman('extract', '-i', image_fname, 'u-boot', '-f', fname) | ||||||
|  |         data = tools.ReadFile(fname) | ||||||
|  |         self.assertEqual(U_BOOT_DATA, data) | ||||||
|  |  | ||||||
|  |     def testExtractOneEntry(self): | ||||||
|  |         """Test extracting a single entry fron an image """ | ||||||
|  |         self._CheckLz4() | ||||||
|  |         self._DoReadFileRealDtb('130_list_fdtmap.dts') | ||||||
|  |         image_fname = tools.GetOutputFilename('image.bin') | ||||||
|  |         fname = os.path.join(self._indir, 'output.extact') | ||||||
|  |         control.ExtractEntries(image_fname, fname, None, ['u-boot']) | ||||||
|  |         data = tools.ReadFile(fname) | ||||||
|  |         self.assertEqual(U_BOOT_DATA, data) | ||||||
|  |  | ||||||
|  |     def _CheckExtractOutput(self, decomp): | ||||||
|  |         """Helper to test file output with and without decompression | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             decomp: True to decompress entry data, False to output it raw | ||||||
|  |         """ | ||||||
|  |         def _CheckPresent(entry_path, expect_data, expect_size=None): | ||||||
|  |             """Check and remove expected file | ||||||
|  |  | ||||||
|  |             This checks the data/size of a file and removes the file both from | ||||||
|  |             the outfiles set and from the output directory. Once all files are | ||||||
|  |             processed, both the set and directory should be empty. | ||||||
|  |  | ||||||
|  |             Args: | ||||||
|  |                 entry_path: Entry path | ||||||
|  |                 expect_data: Data to expect in file, or None to skip check | ||||||
|  |                 expect_size: Size of data to expect in file, or None to skip | ||||||
|  |             """ | ||||||
|  |             path = os.path.join(outdir, entry_path) | ||||||
|  |             data = tools.ReadFile(path) | ||||||
|  |             os.remove(path) | ||||||
|  |             if expect_data: | ||||||
|  |                 self.assertEqual(expect_data, data) | ||||||
|  |             elif expect_size: | ||||||
|  |                 self.assertEqual(expect_size, len(data)) | ||||||
|  |             outfiles.remove(path) | ||||||
|  |  | ||||||
|  |         def _CheckDirPresent(name): | ||||||
|  |             """Remove expected directory | ||||||
|  |  | ||||||
|  |             This gives an error if the directory does not exist as expected | ||||||
|  |  | ||||||
|  |             Args: | ||||||
|  |                 name: Name of directory to remove | ||||||
|  |             """ | ||||||
|  |             path = os.path.join(outdir, name) | ||||||
|  |             os.rmdir(path) | ||||||
|  |  | ||||||
|  |         self._DoReadFileRealDtb('130_list_fdtmap.dts') | ||||||
|  |         image_fname = tools.GetOutputFilename('image.bin') | ||||||
|  |         outdir = os.path.join(self._indir, 'extract') | ||||||
|  |         einfos = control.ExtractEntries(image_fname, None, outdir, [], decomp) | ||||||
|  |  | ||||||
|  |         # Create a set of all file that were output (should be 9) | ||||||
|  |         outfiles = set() | ||||||
|  |         for root, dirs, files in os.walk(outdir): | ||||||
|  |             outfiles |= set([os.path.join(root, fname) for fname in files]) | ||||||
|  |         self.assertEqual(9, len(outfiles)) | ||||||
|  |         self.assertEqual(9, len(einfos)) | ||||||
|  |  | ||||||
|  |         image = control.images['image'] | ||||||
|  |         entries = image.GetEntries() | ||||||
|  |  | ||||||
|  |         # Check the 9 files in various ways | ||||||
|  |         section = entries['section'] | ||||||
|  |         section_entries = section.GetEntries() | ||||||
|  |         cbfs_entries = section_entries['cbfs'].GetEntries() | ||||||
|  |         _CheckPresent('u-boot', U_BOOT_DATA) | ||||||
|  |         _CheckPresent('section/cbfs/u-boot', U_BOOT_DATA) | ||||||
|  |         dtb_len = EXTRACT_DTB_SIZE | ||||||
|  |         if not decomp: | ||||||
|  |             dtb_len = cbfs_entries['u-boot-dtb'].size | ||||||
|  |         _CheckPresent('section/cbfs/u-boot-dtb', None, dtb_len) | ||||||
|  |         if not decomp: | ||||||
|  |             dtb_len = section_entries['u-boot-dtb'].size | ||||||
|  |         _CheckPresent('section/u-boot-dtb', None, dtb_len) | ||||||
|  |  | ||||||
|  |         fdtmap = entries['fdtmap'] | ||||||
|  |         _CheckPresent('fdtmap', fdtmap.data) | ||||||
|  |         hdr = entries['image-header'] | ||||||
|  |         _CheckPresent('image-header', hdr.data) | ||||||
|  |  | ||||||
|  |         _CheckPresent('section/root', section.data) | ||||||
|  |         cbfs = section_entries['cbfs'] | ||||||
|  |         _CheckPresent('section/cbfs/root', cbfs.data) | ||||||
|  |         data = tools.ReadFile(image_fname) | ||||||
|  |         _CheckPresent('root', data) | ||||||
|  |  | ||||||
|  |         # There should be no files left. Remove all the directories to check. | ||||||
|  |         # If there are any files/dirs remaining, one of these checks will fail. | ||||||
|  |         self.assertEqual(0, len(outfiles)) | ||||||
|  |         _CheckDirPresent('section/cbfs') | ||||||
|  |         _CheckDirPresent('section') | ||||||
|  |         _CheckDirPresent('') | ||||||
|  |         self.assertFalse(os.path.exists(outdir)) | ||||||
|  |  | ||||||
|  |     def testExtractAllEntries(self): | ||||||
|  |         """Test extracting all entries""" | ||||||
|  |         self._CheckLz4() | ||||||
|  |         self._CheckExtractOutput(decomp=True) | ||||||
|  |  | ||||||
|  |     def testExtractAllEntriesRaw(self): | ||||||
|  |         """Test extracting all entries without decompressing them""" | ||||||
|  |         self._CheckLz4() | ||||||
|  |         self._CheckExtractOutput(decomp=False) | ||||||
|  |  | ||||||
|  |     def testExtractSelectedEntries(self): | ||||||
|  |         """Test extracting some entries""" | ||||||
|  |         self._CheckLz4() | ||||||
|  |         self._DoReadFileRealDtb('130_list_fdtmap.dts') | ||||||
|  |         image_fname = tools.GetOutputFilename('image.bin') | ||||||
|  |         outdir = os.path.join(self._indir, 'extract') | ||||||
|  |         einfos = control.ExtractEntries(image_fname, None, outdir, | ||||||
|  |                                         ['*cb*', '*head*']) | ||||||
|  |  | ||||||
|  |         # File output is tested by testExtractAllEntries(), so just check that | ||||||
|  |         # the expected entries are selected | ||||||
|  |         names = [einfo.name for einfo in einfos] | ||||||
|  |         self.assertEqual(names, | ||||||
|  |                          ['cbfs', 'u-boot', 'u-boot-dtb', 'image-header']) | ||||||
|  |  | ||||||
|  |     def testExtractNoEntryPaths(self): | ||||||
|  |         """Test extracting some entries""" | ||||||
|  |         self._CheckLz4() | ||||||
|  |         self._DoReadFileRealDtb('130_list_fdtmap.dts') | ||||||
|  |         image_fname = tools.GetOutputFilename('image.bin') | ||||||
|  |         with self.assertRaises(ValueError) as e: | ||||||
|  |             control.ExtractEntries(image_fname, 'fname', None, []) | ||||||
|  |         self.assertIn('Must specify an entry path to write with -o', | ||||||
|  |                       str(e.exception)) | ||||||
|  |  | ||||||
|  |     def testExtractTooManyEntryPaths(self): | ||||||
|  |         """Test extracting some entries""" | ||||||
|  |         self._CheckLz4() | ||||||
|  |         self._DoReadFileRealDtb('130_list_fdtmap.dts') | ||||||
|  |         image_fname = tools.GetOutputFilename('image.bin') | ||||||
|  |         with self.assertRaises(ValueError) as e: | ||||||
|  |             control.ExtractEntries(image_fname, 'fname', None, ['a', 'b']) | ||||||
|  |         self.assertIn('Must specify exactly one entry path to write with -o', | ||||||
|  |                       str(e.exception)) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     unittest.main() |     unittest.main() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user