rred.cc 16 KB

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