1/*
2 * Copyright 2008, Ingo Weinhold, ingo_weinhold@gmx.de. All rights reserved.
3 *
4 * Distributed under the terms of the MIT License.
5 */
6
7#include <dirent.h>
8#include <errno.h>
9#include <stdio.h>
10#include <stdlib.h>
11#include <string.h>
12#include <sys/stat.h>
13#include <unistd.h>
14
15#include <map>
16#include <string>
17
18
19using std::string;
20using std::map;
21
22
23class Directory;
24class Node;
25
26static Node* create_node(Directory* parent, const string& name,
27	const struct stat& st);
28
29
30enum diff_status {
31	DIFF_UNCHANGED,
32	DIFF_REMOVED,
33	DIFF_CHANGED,
34	DIFF_ERROR
35};
36
37
38class EntryWriter {
39public:
40	EntryWriter(int fd)
41		: fFD(fd)
42	{
43	}
44
45	void Write(const char* entry)
46	{
47		write(fFD, entry, strlen(entry));
48		write(fFD, "\n", 1);
49	}
50
51private:
52	int	fFD;
53};
54
55
56class Node {
57public:
58	Node(Directory* parent, const string& name, const struct stat& st)
59		: fParent(parent),
60		  fName(name),
61		  fStat(st)
62	{
63	}
64
65	virtual ~Node()
66	{
67	}
68
69	Directory* Parent() const		{ return fParent; }
70	const string& Name() const		{ return fName; }
71	const struct stat& Stat() const	{ return fStat; }
72
73	string Path() const;
74
75	bool DoStat(struct stat& st) const
76	{
77		string path(Path());
78		if (lstat(path.c_str(), &st) != 0)
79			return false;
80		return true;
81	}
82
83	virtual bool Scan()
84	{
85		return true;
86	}
87
88	virtual diff_status CollectDiffEntries(EntryWriter* out) const
89	{
90		string path(Path());
91		struct stat st;
92
93		diff_status status = DiffEntry(path, st);
94		if (status == DIFF_CHANGED)
95			out->Write(path.c_str());
96
97		return status;
98	}
99
100	virtual void Dump(int level) const
101	{
102		printf("%*s%s\n", level, "", fName.c_str());
103	}
104
105protected:
106	diff_status DiffEntry(const string& path, struct stat& st) const
107	{
108		if (lstat(path.c_str(), &st) == 0) {
109			if (st.st_mode != fStat.st_mode
110				|| st.st_mtime != fStat.st_mtime
111				|| st.st_size != fStat.st_size) {
112				return DIFF_CHANGED;
113			}
114			return DIFF_UNCHANGED;
115		} else if (errno == ENOENT) {
116			// that's OK, the entry was removed
117			return DIFF_REMOVED;
118		} else {
119			// some error
120			fprintf(stderr, "Error: Failed to stat \"%s\": %s\n",
121				path.c_str(), strerror(errno));
122			return DIFF_ERROR;
123		}
124	}
125
126private:
127	Directory*	fParent;
128	string		fName;
129	struct stat	fStat;
130};
131
132
133class Directory : public Node {
134public:
135	Directory(Directory* parent, const string& name, const struct stat& st)
136		: Node(parent, name, st),
137		  fEntries()
138	{
139	}
140
141	void AddEntry(const char* name, Node* node)
142	{
143		fEntries[name] = node;
144	}
145
146	virtual bool Scan()
147	{
148		string path(Path());
149		DIR* dir = opendir(path.c_str());
150		if (dir == NULL) {
151			fprintf(stderr, "Error: Failed to open directory \"%s\": %s\n",
152				path.c_str(), strerror(errno));
153			return false;
154		}
155
156		errno = 0;
157		while (dirent* entry = readdir(dir)) {
158			if (strcmp(entry->d_name, ".") == 0
159				|| strcmp(entry->d_name, "..") == 0) {
160				continue;
161			}
162
163			string entryPath = path + '/' + entry->d_name;
164			struct stat st;
165			if (lstat(entryPath.c_str(), &st) != 0) {
166				fprintf(stderr, "Error: Failed to stat entry \"%s\": %s\n",
167					entryPath.c_str(), strerror(errno));
168				closedir(dir);
169				return false;
170			}
171
172			Node* node = create_node(this, entry->d_name, st);
173			fEntries[entry->d_name] = node;
174
175			errno = 0;
176		}
177
178		if (errno != 0) {
179			fprintf(stderr, "Error: Failed to read directory \"%s\": %s\n",
180				path.c_str(), strerror(errno));
181			closedir(dir);
182			return false;
183		}
184
185		closedir(dir);
186
187		// recursively scan the entries
188		for (EntryMap::iterator it = fEntries.begin(); it != fEntries.end();
189				++it) {
190			Node* node = it->second;
191			if (!node->Scan())
192				return false;
193		}
194
195		return true;
196	}
197
198	virtual diff_status CollectDiffEntries(EntryWriter* out) const
199	{
200		string path(Path());
201		struct stat st;
202
203		diff_status status = DiffEntry(path, st);
204		if (status == DIFF_REMOVED || status == DIFF_ERROR)
205			return status;
206
207		// we print it only if it is no longer a directory
208		if (!S_ISDIR(st.st_mode)) {
209			out->Write(path.c_str());
210			return DIFF_CHANGED;
211		}
212
213		// iterate through the "new" entries
214		DIR* dir = opendir(path.c_str());
215		if (dir == NULL) {
216			fprintf(stderr, "Error: Failed to open directory \"%s\": %s\n",
217				path.c_str(), strerror(errno));
218			return DIFF_ERROR;
219		}
220
221		errno = 0;
222		while (dirent* entry = readdir(dir)) {
223			if (strcmp(entry->d_name, ".") == 0
224				|| strcmp(entry->d_name, "..") == 0) {
225				continue;
226			}
227
228			EntryMap::const_iterator it = fEntries.find(entry->d_name);
229			if (it == fEntries.end()) {
230				// new entry
231				string entryPath = path + "/" + entry->d_name;
232				out->Write(entryPath.c_str());
233			} else {
234				// old entry -- recurse
235				diff_status entryStatus = it->second->CollectDiffEntries(out);
236				if (entryStatus == DIFF_ERROR) {
237					closedir(dir);
238					return status;
239				}
240				if (entryStatus != DIFF_UNCHANGED)
241					status = DIFF_CHANGED;
242			}
243
244			errno = 0;
245		}
246
247		if (errno != 0) {
248			fprintf(stderr, "Error: Failed to read directory \"%s\": %s\n",
249				path.c_str(), strerror(errno));
250			closedir(dir);
251			return DIFF_ERROR;
252		}
253
254		closedir(dir);
255		return status;
256	}
257
258	virtual void Dump(int level) const
259	{
260		Node::Dump(level);
261
262		for (EntryMap::const_iterator it = fEntries.begin();
263				it != fEntries.end(); ++it) {
264			it->second->Dump(level + 1);
265		}
266	}
267
268
269private:
270	typedef map<string, Node*> EntryMap;
271
272	EntryMap	fEntries;
273};
274
275
276string
277Node::Path() const
278{
279	if (fParent == NULL)
280		return fName;
281
282	return fParent->Path() + '/' + fName;
283}
284
285
286static Node*
287create_node(Directory* parent, const string& name, const struct stat& st)
288{
289	if (S_ISDIR(st.st_mode))
290		return new Directory(parent, name, st);
291	return new Node(parent, name, st);
292}
293
294
295int
296main(int argc, const char* const* argv)
297{
298	// the paths are listed after a "--" argument
299	int argi = 1;
300	for (; argi < argc; argi++) {
301		if (strcmp(argv[argi], "--") == 0)
302			break;
303	}
304
305	if (argi + 1 >= argc) {
306		fprintf(stderr, "Usage: %s <zip arguments> ... -- <paths>\n", argv[0]);
307		exit(1);
308	}
309
310	int zipArgCount = argi;
311	const char* const* paths = argv + argi + 1;
312	int pathCount = argc - argi - 1;
313
314	// snapshot the hierarchy
315	Node** rootNodes = new Node*[pathCount];
316
317	for (int i = 0; i < pathCount; i++) {
318		const char* path = paths[i];
319		struct stat st;
320		if (lstat(path, &st) != 0) {
321			fprintf(stderr, "Error: Failed to stat \"%s\": %s\n", path,
322				strerror(errno));
323			exit(1);
324		}
325
326		rootNodes[i] = create_node(NULL, path, st);
327		if (!rootNodes[i]->Scan())
328			exit(1);
329	}
330
331	// create a temp file
332	char tmpFileName[64];
333	sprintf(tmpFileName, "/tmp/diff_zip_%d_XXXXXX", (int)getpid());
334	int tmpFileFD = mkstemp(tmpFileName);
335	if (tmpFileFD < 0) {
336		fprintf(stderr, "Error: Failed create temp file: %s\n",
337			strerror(errno));
338		exit(1);
339	}
340	unlink(tmpFileName);
341
342	// wait
343	{
344		printf("Waiting for changes. Press RETURN to continue...");
345		fflush(stdout);
346		char buffer[32];
347		fgets(buffer, sizeof(buffer), stdin);
348	}
349
350	EntryWriter tmpFile(tmpFileFD);
351
352	for (int i = 0; i < pathCount; i++) {
353		if (rootNodes[i]->CollectDiffEntries(&tmpFile) == DIFF_ERROR)
354			exit(1);
355	}
356
357	// dup the temp file FD to our stdin and exec()
358	dup2(tmpFileFD, 0);
359	close(tmpFileFD);
360	lseek(0, 0, SEEK_SET);
361
362	char** zipArgs = new char*[zipArgCount + 2];
363	zipArgs[0] = strdup("zip");
364	zipArgs[1] = strdup("-@");
365	for (int i = 1; i < zipArgCount; i++)
366		zipArgs[i + 1] = strdup(argv[i]);
367	zipArgs[zipArgCount + 1] = NULL;
368
369	execvp("zip", zipArgs);
370
371	fprintf(stderr, "Error: Failed exec*() zip: %s\n", strerror(errno));
372	delete[] rootNodes;
373
374	return 1;
375}
376