Clarified copyrights.
[zeroinstall.git] / zeroinstall / zerostore / manifest.py
blobddfbd26a5e4a5adff8f7f211a431a0e63917b26c
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 from __future__ import generators
5 import os, stat
6 from sets import Set
7 import sha
8 from zeroinstall import SafeException
10 """A manifest is a string representing a directory tree, with the property
11 that two trees will generate identical manifest strings if and only if:
13 - They have extactly the same set of files, directories and symlinks.
14 - For each pair of corresponding directories in the two sets:
15 - The mtimes are the same.
16 - For each pair of corresponding files in the two sets:
17 - The size, executable flag and mtime are the same.
18 - The contents have matching SHA1 sums.
19 - For each pair of corresponding symlinks in the two sets:
20 - The mtime and size are the same.
21 - The targets have matching SHA1 sums.
23 The manifest is typically processed with SHA1 itself. So, the idea is that
24 any significant change to the contents of the tree will change the SHA1 sum
25 of the manifest.
27 A top-level ".manifest" file is ignored.
28 """
30 def generate_manifest(root):
31 def recurse(sub):
32 # To ensure that a line-by-line comparison of the manifests
33 # is possible, we require that filenames don't contain newlines.
34 # Otherwise, you can name a file so that the part after the \n
35 # would be interpreted as another line in the manifest.
36 assert '\n' not in sub
37 assert sub.startswith('/')
39 if sub == '/.manifest': return
41 full = os.path.join(root, sub[1:])
42 info = os.lstat(full)
44 m = info.st_mode
45 if stat.S_ISDIR(m):
46 if sub != '/':
47 yield "D %s %s" % (info.st_mtime, sub)
48 items = os.listdir(full)
49 items.sort()
50 for x in items:
51 for y in recurse(os.path.join(sub, x)):
52 yield y
53 return
55 assert sub[1:]
56 leaf = os.path.basename(sub[1:])
57 if stat.S_ISREG(m):
58 d = sha.new(file(full).read()).hexdigest()
59 if m & 0111:
60 yield "X %s %s %s %s" % (d, info.st_mtime,info.st_size, leaf)
61 else:
62 yield "F %s %s %s %s" % (d, info.st_mtime,info.st_size, leaf)
63 elif stat.S_ISLNK(m):
64 d = sha.new(os.readlink(full)).hexdigest()
65 # Note: Can't use utime on symlinks, so skip mtime
66 yield "S %s %s %s" % (d, info.st_size, leaf)
67 else:
68 raise SafeException("Unknown object '%s' (not a file, directory or symlink)" %
69 full)
70 for x in recurse('/'): yield x
72 def add_manifest_file(dir, digest):
73 """Writes a .manifest file into 'dir', and updates digest."""
74 mfile = os.path.join(dir, '.manifest')
75 if os.path.islink(mfile) or os.path.exists(mfile):
76 raise Exception('Archive contains a .manifest file!')
77 manifest = ''
78 for line in generate_manifest(dir):
79 manifest += line + '\n'
80 digest.update(manifest)
81 stream = file(mfile, 'w')
82 stream.write(manifest)
83 stream.close()
84 return digest