blob: 5c933095b20a09fd944e66904554729ddc34e741 [file] [log] [blame]
maruelea586f32016-04-05 11:11:33 -07001# Copyright 2014 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07002# Use of this source code is governed under the Apache License, Version 2.0
3# that can be found in the LICENSE file.
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -04004
5"""Understands .isolated files and can do local operations on them."""
6
7import hashlib
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -04008import json
Marc-Antoine Ruel92257792014-08-28 20:51:08 -04009import logging
10import os
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040011import re
Marc-Antoine Ruel92257792014-08-28 20:51:08 -040012import stat
13import sys
14
15from utils import file_path
maruel12e30012015-10-09 11:55:35 -070016from utils import fs
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -040017from utils import tools
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040018
19
20# Version stored and expected in .isolated files.
tansell26de79e2016-11-13 18:41:11 -080021ISOLATED_FILE_VERSION = '1.6'
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040022
23
24# Chunk size to use when doing disk I/O.
25DISK_FILE_CHUNK = 1024 * 1024
26
27
28# Sadly, hashlib uses 'sha1' instead of the standard 'sha-1' so explicitly
29# specify the names here.
30SUPPORTED_ALGOS = {
31 'md5': hashlib.md5,
32 'sha-1': hashlib.sha1,
33 'sha-512': hashlib.sha512,
34}
35
36
37# Used for serialization.
38SUPPORTED_ALGOS_REVERSE = dict((v, k) for k, v in SUPPORTED_ALGOS.iteritems())
39
tansell26de79e2016-11-13 18:41:11 -080040SUPPORTED_FILE_TYPES = ['basic', 'ar', 'tar']
tanselle4288c32016-07-28 09:45:40 -070041
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040042
Marc-Antoine Ruel1e7658c2014-08-28 19:46:39 -040043class IsolatedError(ValueError):
44 """Generic failure to load a .isolated file."""
45 pass
46
47
48class MappingError(OSError):
49 """Failed to recreate the tree."""
50 pass
51
52
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040053def is_valid_hash(value, algo):
54 """Returns if the value is a valid hash for the corresponding algorithm."""
55 size = 2 * algo().digest_size
56 return bool(re.match(r'^[a-fA-F0-9]{%d}$' % size, value))
57
58
59def get_hash_algo(_namespace):
60 """Return hash algorithm class to use when uploading to given |namespace|."""
61 # TODO(vadimsh): Implement this at some point.
62 return hashlib.sha1
63
64
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -040065def is_namespace_with_compression(namespace):
66 """Returns True if given |namespace| stores compressed objects."""
67 return namespace.endswith(('-gzip', '-deflate'))
68
69
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040070def hash_file(filepath, algo):
71 """Calculates the hash of a file without reading it all in memory at once.
72
73 |algo| should be one of hashlib hashing algorithm.
74 """
75 digest = algo()
maruel12e30012015-10-09 11:55:35 -070076 with fs.open(filepath, 'rb') as f:
Marc-Antoine Ruel8bee66d2014-08-28 19:02:07 -040077 while True:
78 chunk = f.read(DISK_FILE_CHUNK)
79 if not chunk:
80 break
81 digest.update(chunk)
82 return digest.hexdigest()
Marc-Antoine Ruel92257792014-08-28 20:51:08 -040083
84
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -040085class IsolatedFile(object):
86 """Represents a single parsed .isolated file."""
Vadim Shtayura7f7459c2014-09-04 13:25:10 -070087
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -040088 def __init__(self, obj_hash, algo):
89 """|obj_hash| is really the sha-1 of the file."""
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -040090 self.obj_hash = obj_hash
91 self.algo = algo
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -040092
93 # Raw data.
94 self.data = {}
95 # A IsolatedFile instance, one per object in self.includes.
96 self.children = []
97
98 # Set once the .isolated file is loaded.
Vadim Shtayura7f7459c2014-09-04 13:25:10 -070099 self._is_loaded = False
100
101 def __repr__(self):
102 return 'IsolatedFile(%s, loaded: %s)' % (self.obj_hash, self._is_loaded)
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400103
104 def load(self, content):
105 """Verifies the .isolated file is valid and loads this object with the json
106 data.
107 """
108 logging.debug('IsolatedFile.load(%s)' % self.obj_hash)
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700109 assert not self._is_loaded
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400110 self.data = load_isolated(content, self.algo)
111 self.children = [
112 IsolatedFile(i, self.algo) for i in self.data.get('includes', [])
113 ]
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700114 self._is_loaded = True
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400115
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700116 @property
117 def is_loaded(self):
118 """Returns True if 'load' was already called."""
119 return self._is_loaded
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400120
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400121
Vadim Shtayura7f7459c2014-09-04 13:25:10 -0700122def walk_includes(isolated):
123 """Walks IsolatedFile include graph and yields IsolatedFile objects.
124
125 Visits root node first, then recursively all children, left to right.
126 Not yet loaded nodes are considered childless.
127 """
128 yield isolated
129 for child in isolated.children:
130 for x in walk_includes(child):
131 yield x
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400132
133
Vadim Shtayurac28b74f2014-10-06 20:00:08 -0700134@tools.profile
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400135def expand_symlinks(indir, relfile):
136 """Follows symlinks in |relfile|, but treating symlinks that point outside the
137 build tree as if they were ordinary directories/files. Returns the final
138 symlink-free target and a list of paths to symlinks encountered in the
139 process.
140
141 The rule about symlinks outside the build tree is for the benefit of the
142 Chromium OS ebuild, which symlinks the output directory to an unrelated path
143 in the chroot.
144
145 Fails when a directory loop is detected, although in theory we could support
146 that case.
147 """
148 is_directory = relfile.endswith(os.path.sep)
149 done = indir
150 todo = relfile.strip(os.path.sep)
151 symlinks = []
152
153 while todo:
Vadim Shtayura56c17562014-10-07 17:13:34 -0700154 pre_symlink, symlink, post_symlink = file_path.split_at_symlink(done, todo)
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400155 if not symlink:
156 todo = file_path.fix_native_path_case(done, todo)
157 done = os.path.join(done, todo)
158 break
159 symlink_path = os.path.join(done, pre_symlink, symlink)
160 post_symlink = post_symlink.lstrip(os.path.sep)
161 # readlink doesn't exist on Windows.
162 # pylint: disable=E1101
163 target = os.path.normpath(os.path.join(done, pre_symlink))
164 symlink_target = os.readlink(symlink_path)
165 if os.path.isabs(symlink_target):
166 # Absolute path are considered a normal directories. The use case is
167 # generally someone who puts the output directory on a separate drive.
168 target = symlink_target
169 else:
170 # The symlink itself could be using the wrong path case.
171 target = file_path.fix_native_path_case(target, symlink_target)
172
173 if not os.path.exists(target):
174 raise MappingError(
175 'Symlink target doesn\'t exist: %s -> %s' % (symlink_path, target))
176 target = file_path.get_native_path_case(target)
177 if not file_path.path_starts_with(indir, target):
178 done = symlink_path
179 todo = post_symlink
180 continue
181 if file_path.path_starts_with(target, symlink_path):
182 raise MappingError(
183 'Can\'t map recursive symlink reference %s -> %s' %
184 (symlink_path, target))
185 logging.info('Found symlink: %s -> %s', symlink_path, target)
186 symlinks.append(os.path.relpath(symlink_path, indir))
187 # Treat the common prefix of the old and new paths as done, and start
188 # scanning again.
189 target = target.split(os.path.sep)
190 symlink_path = symlink_path.split(os.path.sep)
191 prefix_length = 0
192 for target_piece, symlink_path_piece in zip(target, symlink_path):
193 if target_piece == symlink_path_piece:
194 prefix_length += 1
195 else:
196 break
197 done = os.path.sep.join(target[:prefix_length])
198 todo = os.path.join(
199 os.path.sep.join(target[prefix_length:]), post_symlink)
200
201 relfile = os.path.relpath(done, indir)
202 relfile = relfile.rstrip(os.path.sep) + is_directory * os.path.sep
203 return relfile, symlinks
204
205
Vadim Shtayurac28b74f2014-10-06 20:00:08 -0700206@tools.profile
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400207def expand_directory_and_symlink(indir, relfile, blacklist, follow_symlinks):
208 """Expands a single input. It can result in multiple outputs.
209
210 This function is recursive when relfile is a directory.
211
212 Note: this code doesn't properly handle recursive symlink like one created
213 with:
214 ln -s .. foo
215 """
216 if os.path.isabs(relfile):
217 raise MappingError('Can\'t map absolute path %s' % relfile)
218
219 infile = file_path.normpath(os.path.join(indir, relfile))
220 if not infile.startswith(indir):
221 raise MappingError('Can\'t map file %s outside %s' % (infile, indir))
222
223 filepath = os.path.join(indir, relfile)
224 native_filepath = file_path.get_native_path_case(filepath)
225 if filepath != native_filepath:
226 # Special case './'.
227 if filepath != native_filepath + '.' + os.path.sep:
228 # While it'd be nice to enforce path casing on Windows, it's impractical.
229 # Also give up enforcing strict path case on OSX. Really, it's that sad.
230 # The case where it happens is very specific and hard to reproduce:
231 # get_native_path_case(
232 # u'Foo.framework/Versions/A/Resources/Something.nib') will return
233 # u'Foo.framework/Versions/A/resources/Something.nib', e.g. lowercase 'r'.
234 #
235 # Note that this is really something deep in OSX because running
236 # ls Foo.framework/Versions/A
237 # will print out 'Resources', while file_path.get_native_path_case()
238 # returns a lower case 'r'.
239 #
240 # So *something* is happening under the hood resulting in the command 'ls'
241 # and Carbon.File.FSPathMakeRef('path').FSRefMakePath() to disagree. We
242 # have no idea why.
243 if sys.platform not in ('darwin', 'win32'):
244 raise MappingError(
245 'File path doesn\'t equal native file path\n%s != %s' %
246 (filepath, native_filepath))
247
248 symlinks = []
249 if follow_symlinks:
Marc-Antoine Ruela275b292014-11-25 15:17:21 -0500250 try:
251 relfile, symlinks = expand_symlinks(indir, relfile)
252 except OSError:
253 # The file doesn't exist, it will throw below.
254 pass
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400255
256 if relfile.endswith(os.path.sep):
257 if not os.path.isdir(infile):
258 raise MappingError(
259 '%s is not a directory but ends with "%s"' % (infile, os.path.sep))
260
261 # Special case './'.
262 if relfile.startswith('.' + os.path.sep):
263 relfile = relfile[2:]
264 outfiles = symlinks
265 try:
maruel12e30012015-10-09 11:55:35 -0700266 for filename in fs.listdir(infile):
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400267 inner_relfile = os.path.join(relfile, filename)
268 if blacklist and blacklist(inner_relfile):
269 continue
270 if os.path.isdir(os.path.join(indir, inner_relfile)):
271 inner_relfile += os.path.sep
272 outfiles.extend(
273 expand_directory_and_symlink(indir, inner_relfile, blacklist,
274 follow_symlinks))
275 return outfiles
276 except OSError as e:
277 raise MappingError(
278 'Unable to iterate over directory %s.\n%s' % (infile, e))
279 else:
280 # Always add individual files even if they were blacklisted.
281 if os.path.isdir(infile):
282 raise MappingError(
283 'Input directory %s must have a trailing slash' % infile)
284
285 if not os.path.isfile(infile):
286 raise MappingError('Input file %s doesn\'t exist' % infile)
287
288 return symlinks + [relfile]
289
290
291def expand_directories_and_symlinks(
292 indir, infiles, blacklist, follow_symlinks, ignore_broken_items):
293 """Expands the directories and the symlinks, applies the blacklist and
294 verifies files exist.
295
296 Files are specified in os native path separator.
297 """
298 outfiles = []
299 for relfile in infiles:
300 try:
301 outfiles.extend(
302 expand_directory_and_symlink(
303 indir, relfile, blacklist, follow_symlinks))
304 except MappingError as e:
305 if not ignore_broken_items:
306 raise
307 logging.info('warning: %s', e)
308 return outfiles
309
310
Vadim Shtayurac28b74f2014-10-06 20:00:08 -0700311@tools.profile
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400312def file_to_metadata(filepath, prevdict, read_only, algo):
313 """Processes an input file, a dependency, and return meta data about it.
314
315 Behaviors:
316 - Retrieves the file mode, file size, file timestamp, file link
317 destination if it is a file link and calcultate the SHA-1 of the file's
318 content if the path points to a file and not a symlink.
319
320 Arguments:
321 filepath: File to act on.
322 prevdict: the previous dictionary. It is used to retrieve the cached sha-1
323 to skip recalculating the hash. Optional.
324 read_only: If 1 or 2, the file mode is manipulated. In practice, only save
325 one of 4 modes: 0755 (rwx), 0644 (rw), 0555 (rx), 0444 (r). On
326 windows, mode is not set since all files are 'executable' by
327 default.
328 algo: Hashing algorithm used.
329
330 Returns:
331 The necessary dict to create a entry in the 'files' section of an .isolated
332 file.
333 """
Marc-Antoine Ruelf1d827c2014-11-24 15:22:25 -0500334 # TODO(maruel): None is not a valid value.
335 assert read_only in (None, 0, 1, 2), read_only
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400336 out = {}
337 # Always check the file stat and check if it is a link. The timestamp is used
338 # to know if the file's content/symlink destination should be looked into.
339 # E.g. only reuse from prevdict if the timestamp hasn't changed.
340 # There is the risk of the file's timestamp being reset to its last value
341 # manually while its content changed. We don't protect against that use case.
342 try:
343 filestats = os.lstat(filepath)
344 except OSError:
345 # The file is not present.
346 raise MappingError('%s is missing' % filepath)
347 is_link = stat.S_ISLNK(filestats.st_mode)
348
349 if sys.platform != 'win32':
350 # Ignore file mode on Windows since it's not really useful there.
351 filemode = stat.S_IMODE(filestats.st_mode)
352 # Remove write access for group and all access to 'others'.
353 filemode &= ~(stat.S_IWGRP | stat.S_IRWXO)
354 if read_only:
355 filemode &= ~stat.S_IWUSR
Marc-Antoine Ruela275b292014-11-25 15:17:21 -0500356 if filemode & (stat.S_IXUSR|stat.S_IRGRP) == (stat.S_IXUSR|stat.S_IRGRP):
357 # Only keep x group bit if both x user bit and group read bit are set.
Marc-Antoine Ruel92257792014-08-28 20:51:08 -0400358 filemode |= stat.S_IXGRP
359 else:
360 filemode &= ~stat.S_IXGRP
361 if not is_link:
362 out['m'] = filemode
363
364 # Used to skip recalculating the hash or link destination. Use the most recent
365 # update time.
366 out['t'] = int(round(filestats.st_mtime))
367
368 if not is_link:
369 out['s'] = filestats.st_size
370 # If the timestamp wasn't updated and the file size is still the same, carry
371 # on the sha-1.
372 if (prevdict.get('t') == out['t'] and
373 prevdict.get('s') == out['s']):
374 # Reuse the previous hash if available.
375 out['h'] = prevdict.get('h')
376 if not out.get('h'):
377 out['h'] = hash_file(filepath, algo)
378 else:
379 # If the timestamp wasn't updated, carry on the link destination.
380 if prevdict.get('t') == out['t']:
381 # Reuse the previous link destination if available.
382 out['l'] = prevdict.get('l')
383 if out.get('l') is None:
384 # The link could be in an incorrect path case. In practice, this only
385 # happen on OSX on case insensitive HFS.
386 # TODO(maruel): It'd be better if it was only done once, in
387 # expand_directory_and_symlink(), so it would not be necessary to do again
388 # here.
389 symlink_value = os.readlink(filepath) # pylint: disable=E1101
390 filedir = file_path.get_native_path_case(os.path.dirname(filepath))
391 native_dest = file_path.fix_native_path_case(filedir, symlink_value)
392 out['l'] = os.path.relpath(native_dest, filedir)
393 return out
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400394
395
396def save_isolated(isolated, data):
397 """Writes one or multiple .isolated files.
398
399 Note: this reference implementation does not create child .isolated file so it
400 always returns an empty list.
401
402 Returns the list of child isolated files that are included by |isolated|.
403 """
404 # Make sure the data is valid .isolated data by 'reloading' it.
405 algo = SUPPORTED_ALGOS[data['algo']]
406 load_isolated(json.dumps(data), algo)
407 tools.write_json(isolated, data, True)
408 return []
409
410
marueldf6e95e2016-02-26 19:05:38 -0800411def split_path(path):
412 """Splits a path and return a list with each element."""
413 out = []
414 while path:
415 path, rest = os.path.split(path)
416 if rest:
417 out.append(rest)
418 return out
419
420
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400421def load_isolated(content, algo):
422 """Verifies the .isolated file is valid and loads this object with the json
423 data.
424
425 Arguments:
426 - content: raw serialized content to load.
427 - algo: hashlib algorithm class. Used to confirm the algorithm matches the
428 algorithm used on the Isolate Server.
429 """
430 try:
431 data = json.loads(content)
432 except ValueError:
433 raise IsolatedError('Failed to parse: %s...' % content[:100])
434
435 if not isinstance(data, dict):
436 raise IsolatedError('Expected dict, got %r' % data)
437
438 # Check 'version' first, since it could modify the parsing after.
439 value = data.get('version', '1.0')
440 if not isinstance(value, basestring):
441 raise IsolatedError('Expected string, got %r' % value)
442 try:
443 version = tuple(map(int, value.split('.')))
444 except ValueError:
445 raise IsolatedError('Expected valid version, got %r' % value)
446
447 expected_version = tuple(map(int, ISOLATED_FILE_VERSION.split('.')))
448 # Major version must match.
449 if version[0] != expected_version[0]:
450 raise IsolatedError(
451 'Expected compatible \'%s\' version, got %r' %
452 (ISOLATED_FILE_VERSION, value))
453
454 if algo is None:
455 # TODO(maruel): Remove the default around Jan 2014.
456 # Default the algorithm used in the .isolated file itself, falls back to
457 # 'sha-1' if unspecified.
458 algo = SUPPORTED_ALGOS_REVERSE[data.get('algo', 'sha-1')]
459
460 for key, value in data.iteritems():
461 if key == 'algo':
462 if not isinstance(value, basestring):
463 raise IsolatedError('Expected string, got %r' % value)
464 if value not in SUPPORTED_ALGOS:
465 raise IsolatedError(
466 'Expected one of \'%s\', got %r' %
467 (', '.join(sorted(SUPPORTED_ALGOS)), value))
468 if value != SUPPORTED_ALGOS_REVERSE[algo]:
469 raise IsolatedError(
470 'Expected \'%s\', got %r' % (SUPPORTED_ALGOS_REVERSE[algo], value))
471
472 elif key == 'command':
473 if not isinstance(value, list):
474 raise IsolatedError('Expected list, got %r' % value)
475 if not value:
476 raise IsolatedError('Expected non-empty command')
477 for subvalue in value:
478 if not isinstance(subvalue, basestring):
479 raise IsolatedError('Expected string, got %r' % subvalue)
480
481 elif key == 'files':
482 if not isinstance(value, dict):
483 raise IsolatedError('Expected dict, got %r' % value)
484 for subkey, subvalue in value.iteritems():
485 if not isinstance(subkey, basestring):
486 raise IsolatedError('Expected string, got %r' % subkey)
marueldf6e95e2016-02-26 19:05:38 -0800487 if os.path.isabs(subkey) or subkey.startswith('\\\\'):
488 # Disallow '\\\\', it could UNC on Windows but disallow this
489 # everywhere.
490 raise IsolatedError('File path can\'t be absolute: %r' % subkey)
491 if subkey.endswith(('/', '\\')):
492 raise IsolatedError(
493 'File path can\'t end with \'%s\': %r' % (subkey[-1], subkey))
494 if '..' in split_path(subkey):
495 raise IsolatedError('File path can\'t reference parent: %r' % subkey)
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400496 if not isinstance(subvalue, dict):
497 raise IsolatedError('Expected dict, got %r' % subvalue)
498 for subsubkey, subsubvalue in subvalue.iteritems():
499 if subsubkey == 'l':
500 if not isinstance(subsubvalue, basestring):
501 raise IsolatedError('Expected string, got %r' % subsubvalue)
502 elif subsubkey == 'm':
503 if not isinstance(subsubvalue, int):
504 raise IsolatedError('Expected int, got %r' % subsubvalue)
505 elif subsubkey == 'h':
506 if not is_valid_hash(subsubvalue, algo):
507 raise IsolatedError('Expected sha-1, got %r' % subsubvalue)
508 elif subsubkey == 's':
509 if not isinstance(subsubvalue, (int, long)):
510 raise IsolatedError('Expected int or long, got %r' % subsubvalue)
tanselle4288c32016-07-28 09:45:40 -0700511 elif subsubkey == 't':
512 if subsubvalue not in SUPPORTED_FILE_TYPES:
513 raise IsolatedError('Expected one of \'%s\', got %r' % (
514 ', '.join(sorted(SUPPORTED_FILE_TYPES)), subsubvalue))
Marc-Antoine Ruel52436aa2014-08-28 21:57:57 -0400515 else:
516 raise IsolatedError('Unknown subsubkey %s' % subsubkey)
517 if bool('h' in subvalue) == bool('l' in subvalue):
518 raise IsolatedError(
519 'Need only one of \'h\' (sha-1) or \'l\' (link), got: %r' %
520 subvalue)
521 if bool('h' in subvalue) != bool('s' in subvalue):
522 raise IsolatedError(
523 'Both \'h\' (sha-1) and \'s\' (size) should be set, got: %r' %
524 subvalue)
525 if bool('s' in subvalue) == bool('l' in subvalue):
526 raise IsolatedError(
527 'Need only one of \'s\' (size) or \'l\' (link), got: %r' %
528 subvalue)
529 if bool('l' in subvalue) and bool('m' in subvalue):
530 raise IsolatedError(
531 'Cannot use \'m\' (mode) and \'l\' (link), got: %r' %
532 subvalue)
533
534 elif key == 'includes':
535 if not isinstance(value, list):
536 raise IsolatedError('Expected list, got %r' % value)
537 if not value:
538 raise IsolatedError('Expected non-empty includes list')
539 for subvalue in value:
540 if not is_valid_hash(subvalue, algo):
541 raise IsolatedError('Expected sha-1, got %r' % subvalue)
542
543 elif key == 'os':
544 if version >= (1, 4):
545 raise IsolatedError('Key \'os\' is not allowed starting version 1.4')
546
547 elif key == 'read_only':
548 if not value in (0, 1, 2):
549 raise IsolatedError('Expected 0, 1 or 2, got %r' % value)
550
551 elif key == 'relative_cwd':
552 if not isinstance(value, basestring):
553 raise IsolatedError('Expected string, got %r' % value)
554
555 elif key == 'version':
556 # Already checked above.
557 pass
558
559 else:
560 raise IsolatedError('Unknown key %r' % key)
561
562 # Automatically fix os.path.sep if necessary. While .isolated files are always
563 # in the the native path format, someone could want to download an .isolated
564 # tree from another OS.
565 wrong_path_sep = '/' if os.path.sep == '\\' else '\\'
566 if 'files' in data:
567 data['files'] = dict(
568 (k.replace(wrong_path_sep, os.path.sep), v)
569 for k, v in data['files'].iteritems())
570 for v in data['files'].itervalues():
571 if 'l' in v:
572 v['l'] = v['l'].replace(wrong_path_sep, os.path.sep)
573 if 'relative_cwd' in data:
574 data['relative_cwd'] = data['relative_cwd'].replace(
575 wrong_path_sep, os.path.sep)
576 return data