merged from lp:~donkult/apt/sid/
[ntk/apt.git] / methods / rred.cc
1 // Includes /*{{{*/
2 #include <config.h>
3
4 #include <apt-pkg/fileutl.h>
5 #include <apt-pkg/mmap.h>
6 #include <apt-pkg/error.h>
7 #include <apt-pkg/acquire-method.h>
8 #include <apt-pkg/strutl.h>
9 #include <apt-pkg/hashes.h>
10 #include <apt-pkg/configuration.h>
11
12 #include <sys/stat.h>
13 #include <sys/uio.h>
14 #include <unistd.h>
15 #include <utime.h>
16 #include <stdio.h>
17 #include <errno.h>
18 #include <zlib.h>
19 #include <apti18n.h>
20 /*}}}*/
21 /** \brief RredMethod - ed-style incremential patch method {{{
22 *
23 * This method implements a patch functionality similar to "patch --ed" that is
24 * used by the "tiffany" incremental packages download stuff. It differs from
25 * "ed" insofar that it is way more restricted (and therefore secure).
26 * The currently supported ed commands are "<em>c</em>hange", "<em>a</em>dd" and
27 * "<em>d</em>elete" (diff doesn't output any other).
28 * Additionally the records must be reverse sorted by line number and
29 * may not overlap (diff *seems* to produce this kind of output).
30 * */
31 class RredMethod : public pkgAcqMethod {
32 bool Debug;
33 // the size of this doesn't really matter (except for performance)
34 const static int BUF_SIZE = 1024;
35 // the supported ed commands
36 enum Mode {MODE_CHANGED='c', MODE_DELETED='d', MODE_ADDED='a'};
37 // return values
38 enum State {ED_OK, ED_ORDERING, ED_PARSER, ED_FAILURE, MMAP_FAILED};
39
40 State applyFile(FileFd &ed_cmds, FileFd &in_file, FileFd &out_file,
41 unsigned long &line, char *buffer, Hashes *hash) const;
42 void ignoreLineInFile(FileFd &fin, char *buffer) const;
43 void copyLinesFromFileToFile(FileFd &fin, FileFd &fout, unsigned int lines,
44 Hashes *hash, char *buffer) const;
45
46 State patchFile(FileFd &Patch, FileFd &From, FileFd &out_file, Hashes *hash) const;
47 State patchMMap(FileFd &Patch, FileFd &From, FileFd &out_file, Hashes *hash) const;
48
49 protected:
50 // the methods main method
51 virtual bool Fetch(FetchItem *Itm);
52
53 public:
54 RredMethod() : pkgAcqMethod("1.1",SingleInstance | SendConfig), Debug(false) {};
55 };
56 /*}}}*/
57 /** \brief applyFile - in reverse order with a tail recursion {{{
58 *
59 * As it is expected that the commands are in reversed order in the patch file
60 * we check in the first half if the command is valid, but doesn't execute it
61 * and move a step deeper. After reaching the end of the file we apply the
62 * patches in the correct order: last found command first.
63 *
64 * \param ed_cmds patch file to apply
65 * \param in_file base file we want to patch
66 * \param out_file file to write the patched result to
67 * \param line of command operation
68 * \param buffer internal used read/write buffer
69 * \param hash the created file for correctness
70 * \return the success State of the ed command executor
71 */
72 RredMethod::State RredMethod::applyFile(FileFd &ed_cmds, FileFd &in_file, FileFd &out_file,
73 unsigned long &line, char *buffer, Hashes *hash) const {
74 // get the current command and parse it
75 if (ed_cmds.ReadLine(buffer, BUF_SIZE) == NULL) {
76 if (Debug == true)
77 std::clog << "rred: encounter end of file - we can start patching now." << std::endl;
78 line = 0;
79 return ED_OK;
80 }
81
82 // parse in the effected linenumbers
83 char* idx;
84 errno=0;
85 unsigned long const startline = strtol(buffer, &idx, 10);
86 if (errno == ERANGE || errno == EINVAL) {
87 _error->Errno("rred", "startline is an invalid number");
88 return ED_PARSER;
89 }
90 if (startline > line) {
91 _error->Error("rred: The start line (%lu) of the next command is higher than the last line (%lu). This is not allowed.", startline, line);
92 return ED_ORDERING;
93 }
94 unsigned long stopline;
95 if (*idx == ',') {
96 idx++;
97 errno=0;
98 stopline = strtol(idx, &idx, 10);
99 if (errno == ERANGE || errno == EINVAL) {
100 _error->Errno("rred", "stopline is an invalid number");
101 return ED_PARSER;
102 }
103 }
104 else {
105 stopline = startline;
106 }
107 line = startline;
108
109 // which command to execute on this line(s)?
110 switch (*idx) {
111 case MODE_CHANGED:
112 if (Debug == true)
113 std::clog << "Change from line " << startline << " to " << stopline << std::endl;
114 break;
115 case MODE_ADDED:
116 if (Debug == true)
117 std::clog << "Insert after line " << startline << std::endl;
118 break;
119 case MODE_DELETED:
120 if (Debug == true)
121 std::clog << "Delete from line " << startline << " to " << stopline << std::endl;
122 break;
123 default:
124 _error->Error("rred: Unknown ed command '%c'. Abort.", *idx);
125 return ED_PARSER;
126 }
127 unsigned char mode = *idx;
128
129 // save the current position
130 unsigned const long long pos = ed_cmds.Tell();
131
132 // if this is add or change then go to the next full stop
133 unsigned int data_length = 0;
134 if (mode == MODE_CHANGED || mode == MODE_ADDED) {
135 do {
136 ignoreLineInFile(ed_cmds, buffer);
137 data_length++;
138 }
139 while (strncmp(buffer, ".", 1) != 0);
140 data_length--; // the dot should not be copied
141 }
142
143 // do the recursive call - the last command is the one we need to execute at first
144 const State child = applyFile(ed_cmds, in_file, out_file, line, buffer, hash);
145 if (child != ED_OK) {
146 return child;
147 }
148
149 // change and delete are working on "line" - add is done after "line"
150 if (mode != MODE_ADDED)
151 line++;
152
153 // first wind to the current position and copy over all unchanged lines
154 if (line < startline) {
155 copyLinesFromFileToFile(in_file, out_file, (startline - line), hash, buffer);
156 line = startline;
157 }
158
159 if (mode != MODE_ADDED)
160 line--;
161
162 // include data from ed script
163 if (mode == MODE_CHANGED || mode == MODE_ADDED) {
164 ed_cmds.Seek(pos);
165 copyLinesFromFileToFile(ed_cmds, out_file, data_length, hash, buffer);
166 }
167
168 // ignore the corresponding number of lines from input
169 if (mode == MODE_CHANGED || mode == MODE_DELETED) {
170 while (line < stopline) {
171 ignoreLineInFile(in_file, buffer);
172 line++;
173 }
174 }
175 return ED_OK;
176 }
177 /*}}}*/
178 void RredMethod::copyLinesFromFileToFile(FileFd &fin, FileFd &fout, unsigned int lines,/*{{{*/
179 Hashes *hash, char *buffer) const {
180 while (0 < lines--) {
181 do {
182 fin.ReadLine(buffer, BUF_SIZE);
183 unsigned long long const towrite = strlen(buffer);
184 fout.Write(buffer, towrite);
185 hash->Add((unsigned char*)buffer, towrite);
186 } while (strlen(buffer) == (BUF_SIZE - 1) &&
187 buffer[BUF_SIZE - 2] != '\n');
188 }
189 }
190 /*}}}*/
191 void RredMethod::ignoreLineInFile(FileFd &fin, char *buffer) const { /*{{{*/
192 fin.ReadLine(buffer, BUF_SIZE);
193 while (strlen(buffer) == (BUF_SIZE - 1) &&
194 buffer[BUF_SIZE - 2] != '\n') {
195 fin.ReadLine(buffer, BUF_SIZE);
196 buffer[0] = ' ';
197 }
198 }
199 /*}}}*/
200 RredMethod::State RredMethod::patchFile(FileFd &Patch, FileFd &From, /*{{{*/
201 FileFd &out_file, Hashes *hash) const {
202 char buffer[BUF_SIZE];
203
204 /* we do a tail recursion to read the commands in the right order */
205 unsigned long line = -1; // assign highest possible value
206 State const result = applyFile(Patch, From, out_file, line, buffer, hash);
207
208 /* read the rest from infile */
209 if (result == ED_OK) {
210 while (From.ReadLine(buffer, BUF_SIZE) != NULL) {
211 unsigned long long const towrite = strlen(buffer);
212 out_file.Write(buffer, towrite);
213 hash->Add((unsigned char*)buffer, towrite);
214 }
215 }
216 return result;
217 }
218 /*}}}*/
219 /* struct EdCommand {{{*/
220 #ifdef _POSIX_MAPPED_FILES
221 struct EdCommand {
222 size_t data_start;
223 size_t data_end;
224 size_t data_lines;
225 size_t first_line;
226 size_t last_line;
227 char type;
228 };
229 #define IOV_COUNT 1024 /* Don't really want IOV_MAX since it can be arbitrarily large */
230 #endif
231 /*}}}*/
232 RredMethod::State RredMethod::patchMMap(FileFd &Patch, FileFd &From, /*{{{*/
233 FileFd &out_file, Hashes *hash) const {
234 #ifdef _POSIX_MAPPED_FILES
235 MMap ed_cmds(Patch, MMap::ReadOnly);
236 MMap in_file(From, MMap::ReadOnly);
237
238 if (ed_cmds.Size() == 0 || in_file.Size() == 0)
239 return MMAP_FAILED;
240
241 EdCommand* commands = 0;
242 size_t command_count = 0;
243 size_t command_alloc = 0;
244
245 const char* begin = (char*) ed_cmds.Data();
246 const char* end = begin;
247 const char* ed_end = (char*) ed_cmds.Data() + ed_cmds.Size();
248
249 const char* input = (char*) in_file.Data();
250 const char* input_end = (char*) in_file.Data() + in_file.Size();
251
252 size_t i;
253
254 /* 1. Parse entire script. It is executed in reverse order, so we cather it
255 * in the `commands' buffer first
256 */
257
258 for(;;) {
259 EdCommand cmd;
260 cmd.data_start = 0;
261 cmd.data_end = 0;
262
263 while(begin != ed_end && *begin == '\n')
264 ++begin;
265 while(end != ed_end && *end != '\n')
266 ++end;
267 if(end == ed_end && begin == end)
268 break;
269
270 /* Determine command range */
271 const char* tmp = begin;
272
273 for(;;) {
274 /* atoll is safe despite lacking NUL-termination; we know there's an
275 * alphabetic character at end[-1]
276 */
277 if(tmp == end) {
278 cmd.first_line = atol(begin);
279 cmd.last_line = cmd.first_line;
280 break;
281 }
282 if(*tmp == ',') {
283 cmd.first_line = atol(begin);
284 cmd.last_line = atol(tmp + 1);
285 break;
286 }
287 ++tmp;
288 }
289
290 // which command to execute on this line(s)?
291 switch (end[-1]) {
292 case MODE_CHANGED:
293 if (Debug == true)
294 std::clog << "Change from line " << cmd.first_line << " to " << cmd.last_line << std::endl;
295 break;
296 case MODE_ADDED:
297 if (Debug == true)
298 std::clog << "Insert after line " << cmd.first_line << std::endl;
299 break;
300 case MODE_DELETED:
301 if (Debug == true)
302 std::clog << "Delete from line " << cmd.first_line << " to " << cmd.last_line << std::endl;
303 break;
304 default:
305 _error->Error("rred: Unknown ed command '%c'. Abort.", end[-1]);
306 free(commands);
307 return ED_PARSER;
308 }
309 cmd.type = end[-1];
310
311 /* Determine the size of the inserted text, so we don't have to scan this
312 * text again later.
313 */
314 begin = end + 1;
315 end = begin;
316 cmd.data_lines = 0;
317
318 if(cmd.type == MODE_ADDED || cmd.type == MODE_CHANGED) {
319 cmd.data_start = begin - (char*) ed_cmds.Data();
320 while(end != ed_end) {
321 if(*end == '\n') {
322 if(end[-1] == '.' && end[-2] == '\n')
323 break;
324 ++cmd.data_lines;
325 }
326 ++end;
327 }
328 cmd.data_end = end - (char*) ed_cmds.Data() - 1;
329 begin = end + 1;
330 end = begin;
331 }
332 if(command_count == command_alloc) {
333 command_alloc = (command_alloc + 64) * 3 / 2;
334 commands = (EdCommand*) realloc(commands, command_alloc * sizeof(EdCommand));
335 }
336 commands[command_count++] = cmd;
337 }
338
339 struct iovec* iov = new struct iovec[IOV_COUNT];
340 size_t iov_size = 0;
341
342 size_t amount, remaining;
343 size_t line = 1;
344 EdCommand* cmd;
345
346 /* 2. Execute script. We gather writes in a `struct iov' array, and flush
347 * using writev to minimize the number of system calls. Data is read
348 * directly from the memory mappings of the input file and the script.
349 */
350
351 for(i = command_count; i-- > 0; ) {
352 cmd = &commands[i];
353 if(cmd->type == MODE_ADDED)
354 amount = cmd->first_line + 1;
355 else
356 amount = cmd->first_line;
357
358 if(line < amount) {
359 begin = input;
360 while(line != amount) {
361 input = (const char*) memchr(input, '\n', input_end - input);
362 if(!input)
363 break;
364 ++line;
365 ++input;
366 }
367
368 iov[iov_size].iov_base = (void*) begin;
369 iov[iov_size].iov_len = input - begin;
370 hash->Add((const unsigned char*) begin, input - begin);
371
372 if(++iov_size == IOV_COUNT) {
373 writev(out_file.Fd(), iov, IOV_COUNT);
374 iov_size = 0;
375 }
376 }
377
378 if(cmd->type == MODE_DELETED || cmd->type == MODE_CHANGED) {
379 remaining = (cmd->last_line - cmd->first_line) + 1;
380 line += remaining;
381 while(remaining) {
382 input = (const char*) memchr(input, '\n', input_end - input);
383 if(!input)
384 break;
385 --remaining;
386 ++input;
387 }
388 }
389
390 if(cmd->type == MODE_CHANGED || cmd->type == MODE_ADDED) {
391 if(cmd->data_end != cmd->data_start) {
392 iov[iov_size].iov_base = (void*) ((char*)ed_cmds.Data() + cmd->data_start);
393 iov[iov_size].iov_len = cmd->data_end - cmd->data_start;
394 hash->Add((const unsigned char*) ((char*)ed_cmds.Data() + cmd->data_start),
395 iov[iov_size].iov_len);
396
397 if(++iov_size == IOV_COUNT) {
398 writev(out_file.Fd(), iov, IOV_COUNT);
399 iov_size = 0;
400 }
401 }
402 }
403 }
404
405 if(input != input_end) {
406 iov[iov_size].iov_base = (void*) input;
407 iov[iov_size].iov_len = input_end - input;
408 hash->Add((const unsigned char*) input, input_end - input);
409 ++iov_size;
410 }
411
412 if(iov_size) {
413 writev(out_file.Fd(), iov, iov_size);
414 iov_size = 0;
415 }
416
417 for(i = 0; i < iov_size; i += IOV_COUNT) {
418 if(iov_size - i < IOV_COUNT)
419 writev(out_file.Fd(), iov + i, iov_size - i);
420 else
421 writev(out_file.Fd(), iov + i, IOV_COUNT);
422 }
423
424 delete [] iov;
425 free(commands);
426
427 return ED_OK;
428 #else
429 return MMAP_FAILED;
430 #endif
431 }
432 /*}}}*/
433 bool RredMethod::Fetch(FetchItem *Itm) /*{{{*/
434 {
435 Debug = _config->FindB("Debug::pkgAcquire::RRed", false);
436 URI Get = Itm->Uri;
437 std::string Path = Get.Host + Get.Path; // To account for relative paths
438
439 FetchResult Res;
440 Res.Filename = Itm->DestFile;
441 if (Itm->Uri.empty() == true) {
442 Path = Itm->DestFile;
443 Itm->DestFile.append(".result");
444 } else
445 URIStart(Res);
446
447 if (Debug == true)
448 std::clog << "Patching " << Path << " with " << Path
449 << ".ed and putting result into " << Itm->DestFile << std::endl;
450 // Open the source and destination files (the d'tor of FileFd will do
451 // the cleanup/closing of the fds)
452 FileFd From(Path,FileFd::ReadOnly);
453 FileFd Patch(Path+".ed",FileFd::ReadOnly, FileFd::Gzip);
454 FileFd To(Itm->DestFile,FileFd::WriteAtomic);
455 To.EraseOnFailure();
456 if (_error->PendingError() == true)
457 return false;
458
459 Hashes Hash;
460 // now do the actual patching
461 State const result = patchMMap(Patch, From, To, &Hash);
462 if (result == MMAP_FAILED) {
463 // retry with patchFile
464 Patch.Seek(0);
465 From.Seek(0);
466 To.Open(Itm->DestFile,FileFd::WriteAtomic);
467 if (_error->PendingError() == true)
468 return false;
469 if (patchFile(Patch, From, To, &Hash) != ED_OK) {
470 return _error->WarningE("rred", _("Could not patch %s with mmap and with file operation usage - the patch seems to be corrupt."), Path.c_str());
471 } else if (Debug == true) {
472 std::clog << "rred: finished file patching of " << Path << " after mmap failed." << std::endl;
473 }
474 } else if (result != ED_OK) {
475 return _error->Errno("rred", _("Could not patch %s with mmap (but no mmap specific fail) - the patch seems to be corrupt."), Path.c_str());
476 } else if (Debug == true) {
477 std::clog << "rred: finished mmap patching of " << Path << std::endl;
478 }
479
480 // write out the result
481 From.Close();
482 Patch.Close();
483 To.Close();
484
485 /* Transfer the modification times from the patch file
486 to be able to see in which state the file should be
487 and use the access time from the "old" file */
488 struct stat BufBase, BufPatch;
489 if (stat(Path.c_str(),&BufBase) != 0 ||
490 stat(std::string(Path+".ed").c_str(),&BufPatch) != 0)
491 return _error->Errno("stat",_("Failed to stat"));
492
493 struct utimbuf TimeBuf;
494 TimeBuf.actime = BufBase.st_atime;
495 TimeBuf.modtime = BufPatch.st_mtime;
496 if (utime(Itm->DestFile.c_str(),&TimeBuf) != 0)
497 return _error->Errno("utime",_("Failed to set modification time"));
498
499 if (stat(Itm->DestFile.c_str(),&BufBase) != 0)
500 return _error->Errno("stat",_("Failed to stat"));
501
502 // return done
503 Res.LastModified = BufBase.st_mtime;
504 Res.Size = BufBase.st_size;
505 Res.TakeHashes(Hash);
506 URIDone(Res);
507
508 return true;
509 }
510 /*}}}*/
511 /** \brief Wrapper class for testing rred */ /*{{{*/
512 class TestRredMethod : public RredMethod {
513 public:
514 /** \brief Run rred in debug test mode
515 *
516 * This method can be used to run the rred method outside
517 * of the "normal" acquire environment for easier testing.
518 *
519 * \param base basename of all files involved in this rred test
520 */
521 bool Run(char const *base) {
522 _config->CndSet("Debug::pkgAcquire::RRed", "true");
523 FetchItem *test = new FetchItem;
524 test->DestFile = base;
525 return Fetch(test);
526 }
527 };
528 /*}}}*/
529 /** \brief Starter for the rred method (or its test method) {{{
530 *
531 * Used without parameters is the normal behavior for methods for
532 * the APT acquire system. While this works great for the acquire system
533 * it is very hard to test the method and therefore the method also
534 * accepts one parameter which will switch it directly to debug test mode:
535 * The test mode expects that if "Testfile" is given as parameter
536 * the file "Testfile" should be ed-style patched with "Testfile.ed"
537 * and will write the result to "Testfile.result".
538 */
539 int main(int argc, char *argv[]) {
540 if (argc <= 1) {
541 RredMethod Mth;
542 return Mth.Run();
543 } else {
544 TestRredMethod Mth;
545 bool result = Mth.Run(argv[1]);
546 _error->DumpErrors();
547 return result;
548 }
549 }
550 /*}}}*/