blob: 8c6eae83f158367f6f626bf2f3443d479c250390 [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass2574ef62016-11-25 20:15:51 -07002# Copyright (c) 2016 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
Simon Glass2574ef62016-11-25 20:15:51 -07005# Creates binary images from input files controlled by a description
6#
7
8from collections import OrderedDict
9import os
10import sys
Simon Glassa997ea52020-04-17 18:09:04 -060011from patman import tools
Simon Glass2574ef62016-11-25 20:15:51 -070012
Simon Glassc585dd42020-04-17 18:09:03 -060013from binman import cbfs_util
14from binman import elf
Simon Glassa997ea52020-04-17 18:09:04 -060015from patman import command
16from patman import tout
Simon Glass2574ef62016-11-25 20:15:51 -070017
18# List of images we plan to create
19# Make this global so that it can be referenced from tests
20images = OrderedDict()
21
22def _ReadImageDesc(binman_node):
23 """Read the image descriptions from the /binman node
24
25 This normally produces a single Image object called 'image'. But if
26 multiple images are present, they will all be returned.
27
28 Args:
29 binman_node: Node object of the /binman node
30 Returns:
31 OrderedDict of Image objects, each of which describes an image
32 """
33 images = OrderedDict()
34 if 'multiple-images' in binman_node.props:
35 for node in binman_node.subnodes:
36 images[node.name] = Image(node.name, node)
37 else:
38 images['image'] = Image('image', binman_node)
39 return images
40
Simon Glass22c92ca2017-05-27 07:38:29 -060041def _FindBinmanNode(dtb):
Simon Glass2574ef62016-11-25 20:15:51 -070042 """Find the 'binman' node in the device tree
43
44 Args:
Simon Glass22c92ca2017-05-27 07:38:29 -060045 dtb: Fdt object to scan
Simon Glass2574ef62016-11-25 20:15:51 -070046 Returns:
47 Node object of /binman node, or None if not found
48 """
Simon Glass22c92ca2017-05-27 07:38:29 -060049 for node in dtb.GetRoot().subnodes:
Simon Glass2574ef62016-11-25 20:15:51 -070050 if node.name == 'binman':
51 return node
52 return None
53
Simon Glass29aa7362018-09-14 04:57:19 -060054def WriteEntryDocs(modules, test_missing=None):
55 """Write out documentation for all entries
Simon Glass92307732018-07-06 10:27:40 -060056
57 Args:
Simon Glass29aa7362018-09-14 04:57:19 -060058 modules: List of Module objects to get docs for
59 test_missing: Used for testing only, to force an entry's documeentation
60 to show as missing even if it is present. Should be set to None in
61 normal use.
Simon Glass92307732018-07-06 10:27:40 -060062 """
Simon Glassc585dd42020-04-17 18:09:03 -060063 from binman.entry import Entry
Simon Glass969616c2018-07-17 13:25:36 -060064 Entry.WriteDocs(modules, test_missing)
65
Simon Glassb2fd11d2019-07-08 14:25:48 -060066
67def ListEntries(image_fname, entry_paths):
68 """List the entries in an image
69
70 This decodes the supplied image and displays a table of entries from that
71 image, preceded by a header.
72
73 Args:
74 image_fname: Image filename to process
75 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
76 'section/u-boot'])
77 """
78 image = Image.FromFile(image_fname)
79
80 entries, lines, widths = image.GetListEntries(entry_paths)
81
82 num_columns = len(widths)
83 for linenum, line in enumerate(lines):
84 if linenum == 1:
85 # Print header line
86 print('-' * (sum(widths) + num_columns * 2))
87 out = ''
88 for i, item in enumerate(line):
89 width = -widths[i]
90 if item.startswith('>'):
91 width = -width
92 item = item[1:]
93 txt = '%*s ' % (width, item)
94 out += txt
95 print(out.rstrip())
96
Simon Glass4c613bf2019-07-08 14:25:50 -060097
98def ReadEntry(image_fname, entry_path, decomp=True):
99 """Extract an entry from an image
100
101 This extracts the data from a particular entry in an image
102
103 Args:
104 image_fname: Image filename to process
105 entry_path: Path to entry to extract
106 decomp: True to return uncompressed data, if the data is compress
107 False to return the raw data
108
109 Returns:
110 data extracted from the entry
111 """
Simon Glassb9ba4e02019-08-24 07:22:44 -0600112 global Image
113 from image import Image
114
Simon Glass4c613bf2019-07-08 14:25:50 -0600115 image = Image.FromFile(image_fname)
116 entry = image.FindEntryPath(entry_path)
117 return entry.ReadData(decomp)
118
119
Simon Glass980a2842019-07-08 14:25:52 -0600120def ExtractEntries(image_fname, output_fname, outdir, entry_paths,
121 decomp=True):
122 """Extract the data from one or more entries and write it to files
123
124 Args:
125 image_fname: Image filename to process
126 output_fname: Single output filename to use if extracting one file, None
127 otherwise
128 outdir: Output directory to use (for any number of files), else None
129 entry_paths: List of entry paths to extract
Simon Glassd48f94e2019-07-20 12:24:12 -0600130 decomp: True to decompress the entry data
Simon Glass980a2842019-07-08 14:25:52 -0600131
132 Returns:
133 List of EntryInfo records that were written
134 """
135 image = Image.FromFile(image_fname)
136
137 # Output an entry to a single file, as a special case
138 if output_fname:
139 if not entry_paths:
Simon Glassa772d3f2019-07-20 12:24:14 -0600140 raise ValueError('Must specify an entry path to write with -f')
Simon Glass980a2842019-07-08 14:25:52 -0600141 if len(entry_paths) != 1:
Simon Glassa772d3f2019-07-20 12:24:14 -0600142 raise ValueError('Must specify exactly one entry path to write with -f')
Simon Glass980a2842019-07-08 14:25:52 -0600143 entry = image.FindEntryPath(entry_paths[0])
144 data = entry.ReadData(decomp)
145 tools.WriteFile(output_fname, data)
146 tout.Notice("Wrote %#x bytes to file '%s'" % (len(data), output_fname))
147 return
148
149 # Otherwise we will output to a path given by the entry path of each entry.
150 # This means that entries will appear in subdirectories if they are part of
151 # a sub-section.
152 einfos = image.GetListEntries(entry_paths)[0]
153 tout.Notice('%d entries match and will be written' % len(einfos))
154 for einfo in einfos:
155 entry = einfo.entry
156 data = entry.ReadData(decomp)
157 path = entry.GetPath()[1:]
158 fname = os.path.join(outdir, path)
159
160 # If this entry has children, create a directory for it and put its
161 # data in a file called 'root' in that directory
162 if entry.GetEntries():
163 if not os.path.exists(fname):
164 os.makedirs(fname)
165 fname = os.path.join(fname, 'root')
166 tout.Notice("Write entry '%s' to '%s'" % (entry.GetPath(), fname))
167 tools.WriteFile(fname, data)
168 return einfos
169
170
Simon Glass274bd0e2019-07-20 12:24:13 -0600171def BeforeReplace(image, allow_resize):
172 """Handle getting an image ready for replacing entries in it
173
174 Args:
175 image: Image to prepare
176 """
177 state.PrepareFromLoadedData(image)
178 image.LoadData()
179
180 # If repacking, drop the old offset/size values except for the original
181 # ones, so we are only left with the constraints.
182 if allow_resize:
183 image.ResetForPack()
184
185
186def ReplaceOneEntry(image, entry, data, do_compress, allow_resize):
187 """Handle replacing a single entry an an image
188
189 Args:
190 image: Image to update
191 entry: Entry to write
192 data: Data to replace with
193 do_compress: True to compress the data if needed, False if data is
194 already compressed so should be used as is
195 allow_resize: True to allow entries to change size (this does a re-pack
196 of the entries), False to raise an exception
197 """
198 if not entry.WriteData(data, do_compress):
199 if not image.allow_repack:
200 entry.Raise('Entry data size does not match, but allow-repack is not present for this image')
201 if not allow_resize:
202 entry.Raise('Entry data size does not match, but resize is disabled')
203
204
205def AfterReplace(image, allow_resize, write_map):
206 """Handle write out an image after replacing entries in it
207
208 Args:
209 image: Image to write
210 allow_resize: True to allow entries to change size (this does a re-pack
211 of the entries), False to raise an exception
212 write_map: True to write a map file
213 """
214 tout.Info('Processing image')
215 ProcessImage(image, update_fdt=True, write_map=write_map,
216 get_contents=False, allow_resize=allow_resize)
217
218
219def WriteEntryToImage(image, entry, data, do_compress=True, allow_resize=True,
220 write_map=False):
221 BeforeReplace(image, allow_resize)
222 tout.Info('Writing data to %s' % entry.GetPath())
223 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
224 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
225
226
Simon Glassd48f94e2019-07-20 12:24:12 -0600227def WriteEntry(image_fname, entry_path, data, do_compress=True,
228 allow_resize=True, write_map=False):
Simon Glass3971c952019-07-20 12:24:11 -0600229 """Replace an entry in an image
230
231 This replaces the data in a particular entry in an image. This size of the
232 new data must match the size of the old data unless allow_resize is True.
233
234 Args:
235 image_fname: Image filename to process
236 entry_path: Path to entry to extract
237 data: Data to replace with
Simon Glassd48f94e2019-07-20 12:24:12 -0600238 do_compress: True to compress the data if needed, False if data is
Simon Glass3971c952019-07-20 12:24:11 -0600239 already compressed so should be used as is
240 allow_resize: True to allow entries to change size (this does a re-pack
241 of the entries), False to raise an exception
Simon Glassd48f94e2019-07-20 12:24:12 -0600242 write_map: True to write a map file
Simon Glass3971c952019-07-20 12:24:11 -0600243
244 Returns:
245 Image object that was updated
246 """
Simon Glass274bd0e2019-07-20 12:24:13 -0600247 tout.Info("Write entry '%s', file '%s'" % (entry_path, image_fname))
Simon Glass3971c952019-07-20 12:24:11 -0600248 image = Image.FromFile(image_fname)
249 entry = image.FindEntryPath(entry_path)
Simon Glass274bd0e2019-07-20 12:24:13 -0600250 WriteEntryToImage(image, entry, data, do_compress=do_compress,
251 allow_resize=allow_resize, write_map=write_map)
Simon Glass3971c952019-07-20 12:24:11 -0600252
Simon Glass3971c952019-07-20 12:24:11 -0600253 return image
254
Simon Glass30033c22019-07-20 12:24:15 -0600255
256def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
257 do_compress=True, allow_resize=True, write_map=False):
258 """Replace the data from one or more entries from input files
259
260 Args:
261 image_fname: Image filename to process
262 input_fname: Single input ilename to use if replacing one file, None
263 otherwise
264 indir: Input directory to use (for any number of files), else None
265 entry_paths: List of entry paths to extract
266 do_compress: True if the input data is uncompressed and may need to be
267 compressed if the entry requires it, False if the data is already
268 compressed.
269 write_map: True to write a map file
270
271 Returns:
272 List of EntryInfo records that were written
273 """
274 image = Image.FromFile(image_fname)
275
276 # Replace an entry from a single file, as a special case
277 if input_fname:
278 if not entry_paths:
279 raise ValueError('Must specify an entry path to read with -f')
280 if len(entry_paths) != 1:
281 raise ValueError('Must specify exactly one entry path to write with -f')
282 entry = image.FindEntryPath(entry_paths[0])
283 data = tools.ReadFile(input_fname)
284 tout.Notice("Read %#x bytes from file '%s'" % (len(data), input_fname))
285 WriteEntryToImage(image, entry, data, do_compress=do_compress,
286 allow_resize=allow_resize, write_map=write_map)
287 return
288
289 # Otherwise we will input from a path given by the entry path of each entry.
290 # This means that files must appear in subdirectories if they are part of
291 # a sub-section.
292 einfos = image.GetListEntries(entry_paths)[0]
293 tout.Notice("Replacing %d matching entries in image '%s'" %
294 (len(einfos), image_fname))
295
296 BeforeReplace(image, allow_resize)
297
298 for einfo in einfos:
299 entry = einfo.entry
300 if entry.GetEntries():
301 tout.Info("Skipping section entry '%s'" % entry.GetPath())
302 continue
303
304 path = entry.GetPath()[1:]
305 fname = os.path.join(indir, path)
306
307 if os.path.exists(fname):
308 tout.Notice("Write entry '%s' from file '%s'" %
309 (entry.GetPath(), fname))
310 data = tools.ReadFile(fname)
311 ReplaceOneEntry(image, entry, data, do_compress, allow_resize)
312 else:
313 tout.Warning("Skipping entry '%s' from missing file '%s'" %
314 (entry.GetPath(), fname))
315
316 AfterReplace(image, allow_resize=allow_resize, write_map=write_map)
317 return image
318
319
Simon Glassd3151ff2019-07-20 12:23:27 -0600320def PrepareImagesAndDtbs(dtb_fname, select_images, update_fdt):
321 """Prepare the images to be processed and select the device tree
322
323 This function:
324 - reads in the device tree
325 - finds and scans the binman node to create all entries
326 - selects which images to build
327 - Updates the device tress with placeholder properties for offset,
328 image-pos, etc.
329
330 Args:
331 dtb_fname: Filename of the device tree file to use (.dts or .dtb)
332 selected_images: List of images to output, or None for all
333 update_fdt: True to update the FDT wth entry offsets, etc.
334 """
335 # Import these here in case libfdt.py is not available, in which case
336 # the above help option still works.
Simon Glassc585dd42020-04-17 18:09:03 -0600337 from dtoc import fdt
338 from dtoc import fdt_util
Simon Glassd3151ff2019-07-20 12:23:27 -0600339 global images
340
341 # Get the device tree ready by compiling it and copying the compiled
342 # output into a file in our output directly. Then scan it for use
343 # in binman.
344 dtb_fname = fdt_util.EnsureCompiled(dtb_fname)
345 fname = tools.GetOutputFilename('u-boot.dtb.out')
346 tools.WriteFile(fname, tools.ReadFile(dtb_fname))
347 dtb = fdt.FdtScan(fname)
348
349 node = _FindBinmanNode(dtb)
350 if not node:
351 raise ValueError("Device tree '%s' does not have a 'binman' "
352 "node" % dtb_fname)
353
354 images = _ReadImageDesc(node)
355
356 if select_images:
357 skip = []
358 new_images = OrderedDict()
359 for name, image in images.items():
360 if name in select_images:
361 new_images[name] = image
362 else:
363 skip.append(name)
364 images = new_images
365 tout.Notice('Skipping images: %s' % ', '.join(skip))
366
367 state.Prepare(images, dtb)
368
369 # Prepare the device tree by making sure that any missing
370 # properties are added (e.g. 'pos' and 'size'). The values of these
371 # may not be correct yet, but we add placeholders so that the
372 # size of the device tree is correct. Later, in
373 # SetCalculatedProperties() we will insert the correct values
374 # without changing the device-tree size, thus ensuring that our
375 # entry offsets remain the same.
376 for image in images.values():
377 image.ExpandEntries()
378 if update_fdt:
379 image.AddMissingProperties()
380 image.ProcessFdt(dtb)
381
Simon Glass5a300602019-07-20 12:23:29 -0600382 for dtb_item in state.GetAllFdts():
Simon Glassd3151ff2019-07-20 12:23:27 -0600383 dtb_item.Sync(auto_resize=True)
384 dtb_item.Pack()
385 dtb_item.Flush()
386 return images
387
388
Simon Glassf8a54bc2019-07-20 12:23:56 -0600389def ProcessImage(image, update_fdt, write_map, get_contents=True,
Simon Glass5d94cc62020-07-09 18:39:38 -0600390 allow_resize=True, allow_missing=False):
Simon Glassb766c5e52019-07-20 12:23:24 -0600391 """Perform all steps for this image, including checking and # writing it.
392
393 This means that errors found with a later image will be reported after
394 earlier images are already completed and written, but that does not seem
395 important.
396
397 Args:
398 image: Image to process
399 update_fdt: True to update the FDT wth entry offsets, etc.
400 write_map: True to write a map file
Simon Glass072959a2019-07-20 12:23:50 -0600401 get_contents: True to get the image contents from files, etc., False if
402 the contents is already present
Simon Glassf8a54bc2019-07-20 12:23:56 -0600403 allow_resize: True to allow entries to change size (this does a re-pack
404 of the entries), False to raise an exception
Simon Glass5d94cc62020-07-09 18:39:38 -0600405 allow_missing: Allow blob_ext objects to be missing
Simon Glassb766c5e52019-07-20 12:23:24 -0600406 """
Simon Glass072959a2019-07-20 12:23:50 -0600407 if get_contents:
Simon Glass5d94cc62020-07-09 18:39:38 -0600408 image.SetAllowMissing(allow_missing)
Simon Glass072959a2019-07-20 12:23:50 -0600409 image.GetEntryContents()
Simon Glassb766c5e52019-07-20 12:23:24 -0600410 image.GetEntryOffsets()
411
412 # We need to pack the entries to figure out where everything
413 # should be placed. This sets the offset/size of each entry.
414 # However, after packing we call ProcessEntryContents() which
415 # may result in an entry changing size. In that case we need to
416 # do another pass. Since the device tree often contains the
417 # final offset/size information we try to make space for this in
418 # AddMissingProperties() above. However, if the device is
419 # compressed we cannot know this compressed size in advance,
420 # since changing an offset from 0x100 to 0x104 (for example) can
421 # alter the compressed size of the device tree. So we need a
422 # third pass for this.
Simon Glass37fdd142019-07-20 12:24:06 -0600423 passes = 5
Simon Glassb766c5e52019-07-20 12:23:24 -0600424 for pack_pass in range(passes):
425 try:
426 image.PackEntries()
427 image.CheckSize()
428 image.CheckEntries()
429 except Exception as e:
430 if write_map:
431 fname = image.WriteMap()
432 print("Wrote map file '%s' to show errors" % fname)
433 raise
434 image.SetImagePos()
435 if update_fdt:
436 image.SetCalculatedProperties()
Simon Glass5a300602019-07-20 12:23:29 -0600437 for dtb_item in state.GetAllFdts():
Simon Glassb766c5e52019-07-20 12:23:24 -0600438 dtb_item.Sync()
Simon Glassf8a54bc2019-07-20 12:23:56 -0600439 dtb_item.Flush()
Simon Glasse5943412019-08-24 07:23:12 -0600440 image.WriteSymbols()
Simon Glassb766c5e52019-07-20 12:23:24 -0600441 sizes_ok = image.ProcessEntryContents()
442 if sizes_ok:
443 break
444 image.ResetForPack()
Simon Glass6bf9b472019-08-24 07:23:13 -0600445 tout.Info('Pack completed after %d pass(es)' % (pack_pass + 1))
Simon Glassb766c5e52019-07-20 12:23:24 -0600446 if not sizes_ok:
Simon Glass9d8ee322019-07-20 12:23:58 -0600447 image.Raise('Entries changed size after packing (tried %s passes)' %
Simon Glassb766c5e52019-07-20 12:23:24 -0600448 passes)
449
Simon Glassb766c5e52019-07-20 12:23:24 -0600450 image.BuildImage()
451 if write_map:
452 image.WriteMap()
453
454
Simon Glassf46732a2019-07-08 14:25:29 -0600455def Binman(args):
Simon Glass2574ef62016-11-25 20:15:51 -0700456 """The main control code for binman
457
458 This assumes that help and test options have already been dealt with. It
459 deals with the core task of building images.
460
461 Args:
Simon Glassf46732a2019-07-08 14:25:29 -0600462 args: Command line arguments Namespace object
Simon Glass2574ef62016-11-25 20:15:51 -0700463 """
Simon Glassb9ba4e02019-08-24 07:22:44 -0600464 global Image
465 global state
466
Simon Glassf46732a2019-07-08 14:25:29 -0600467 if args.full_help:
Simon Glass2574ef62016-11-25 20:15:51 -0700468 pager = os.getenv('PAGER')
469 if not pager:
470 pager = 'more'
471 fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
472 'README')
473 command.Run(pager, fname)
474 return 0
475
Simon Glassb9ba4e02019-08-24 07:22:44 -0600476 # Put these here so that we can import this module without libfdt
477 from image import Image
Simon Glassc585dd42020-04-17 18:09:03 -0600478 from binman import state
Simon Glassb9ba4e02019-08-24 07:22:44 -0600479
Simon Glassdf08cbb2019-09-15 18:10:36 -0600480 if args.cmd in ['ls', 'extract', 'replace']:
Simon Glass9b7f5002019-07-20 12:23:53 -0600481 try:
Simon Glassdf08cbb2019-09-15 18:10:36 -0600482 tout.Init(args.verbosity)
Simon Glass9b7f5002019-07-20 12:23:53 -0600483 tools.PrepareOutputDir(None)
Simon Glassdf08cbb2019-09-15 18:10:36 -0600484 if args.cmd == 'ls':
485 ListEntries(args.image, args.paths)
Simon Glassb2fd11d2019-07-08 14:25:48 -0600486
Simon Glassdf08cbb2019-09-15 18:10:36 -0600487 if args.cmd == 'extract':
488 ExtractEntries(args.image, args.filename, args.outdir, args.paths,
489 not args.uncompressed)
Simon Glass980a2842019-07-08 14:25:52 -0600490
Simon Glassdf08cbb2019-09-15 18:10:36 -0600491 if args.cmd == 'replace':
492 ReplaceEntries(args.image, args.filename, args.indir, args.paths,
493 do_compress=not args.compressed,
494 allow_resize=not args.fix_size, write_map=args.map)
495 except:
496 raise
Simon Glass30033c22019-07-20 12:24:15 -0600497 finally:
498 tools.FinaliseOutputDir()
499 return 0
500
Simon Glass2574ef62016-11-25 20:15:51 -0700501 # Try to figure out which device tree contains our image description
Simon Glassf46732a2019-07-08 14:25:29 -0600502 if args.dt:
503 dtb_fname = args.dt
Simon Glass2574ef62016-11-25 20:15:51 -0700504 else:
Simon Glassf46732a2019-07-08 14:25:29 -0600505 board = args.board
Simon Glass2574ef62016-11-25 20:15:51 -0700506 if not board:
507 raise ValueError('Must provide a board to process (use -b <board>)')
Simon Glassf46732a2019-07-08 14:25:29 -0600508 board_pathname = os.path.join(args.build_dir, board)
Simon Glass2574ef62016-11-25 20:15:51 -0700509 dtb_fname = os.path.join(board_pathname, 'u-boot.dtb')
Simon Glassf46732a2019-07-08 14:25:29 -0600510 if not args.indir:
511 args.indir = ['.']
512 args.indir.append(board_pathname)
Simon Glass2574ef62016-11-25 20:15:51 -0700513
514 try:
Simon Glassf46732a2019-07-08 14:25:29 -0600515 tout.Init(args.verbosity)
516 elf.debug = args.debug
517 cbfs_util.VERBOSE = args.verbosity > 2
518 state.use_fake_dtb = args.fake_dtb
Simon Glass2574ef62016-11-25 20:15:51 -0700519 try:
Simon Glassf46732a2019-07-08 14:25:29 -0600520 tools.SetInputDirs(args.indir)
521 tools.PrepareOutputDir(args.outdir, args.preserve)
522 tools.SetToolPaths(args.toolpath)
523 state.SetEntryArgs(args.entry_arg)
Simon Glass92307732018-07-06 10:27:40 -0600524
Simon Glassd3151ff2019-07-20 12:23:27 -0600525 images = PrepareImagesAndDtbs(dtb_fname, args.image,
526 args.update_fdt)
Simon Glass2574ef62016-11-25 20:15:51 -0700527 for image in images.values():
Simon Glass5d94cc62020-07-09 18:39:38 -0600528 ProcessImage(image, args.update_fdt, args.map,
529 allow_missing=args.allow_missing)
Simon Glassbdb40312018-09-14 04:57:20 -0600530
531 # Write the updated FDTs to our output files
Simon Glass5a300602019-07-20 12:23:29 -0600532 for dtb_item in state.GetAllFdts():
Simon Glassbdb40312018-09-14 04:57:20 -0600533 tools.WriteFile(dtb_item._fname, dtb_item.GetContents())
534
Simon Glass2574ef62016-11-25 20:15:51 -0700535 finally:
536 tools.FinaliseOutputDir()
537 finally:
538 tout.Uninit()
539
540 return 0