Fix up an integer overflow properly - cast was wrong
[jigdo.git] / src / scan.cc
1 /* $Id: scan.cc,v 1.11 2005/07/02 22:05:04 atterer Exp $ -*- C++ -*-
2   __   _
3   |_) /|  Copyright (C) 2001-2002  |  richard@
4   | \/¯|  Richard Atterer          |  atterer.org
5   ¯ '` ¯
6   This program is free software; you can redistribute it and/or modify
7   it under the terms of the GNU General Public License, version 2. See
8   the file COPYING for details.
9
10   Scanning of input files
11
12 */
13
14 #include <config.h>
15
16 #include <iostream>
17 #include <fstream>
18 #include <string>
19 #include <ctype.h>
20 #include <errno.h>
21 #include <string.h>
22 #include <sys/stat.h>
23 #include <sys/types.h>
24 #include <unistd-jigdo.h>
25
26 #include <bstream.hh>
27 #include <compat.hh>
28 #include <configfile.hh>
29 #include <log.hh>
30 #include <scan.hh>
31 #include <string.hh>
32 #include <serialize.hh>
33 //______________________________________________________________________
34
35 DEBUG_UNIT("scan")
36
37 void JigdoCache::ProgressReporter::error(const string& message) {
38   cerr << message << endl;
39 }
40 void JigdoCache::ProgressReporter::info(const string& message) {
41   cerr << message << endl;
42 }
43 void JigdoCache::ProgressReporter::scanningFile(const FilePart*, uint64) { }
44
45 JigdoCache::ProgressReporter JigdoCache::noReport;
46
47 struct stat JigdoCache::fileInfo;
48 //______________________________________________________________________
49
50 #if HAVE_LIBDB
51
52 // How much checksum data do we have per checksum entry?
53 #define CSUM_SIZE (16 + 32) // md5 size + sha256 size
54
55 /* Interpret a string of bytes (out of the file cache) like this:
56
57    4   blockLength (of rsync sum)
58    4   csumBlockLength
59    4   blocks (number of valid md5 blocks in this entry), curr. always >0
60    8   rsyncSum of file start (only valid if blocks > 0)
61   16   fileMD5Sum (only valid if
62                    blocks == (fileSize+csumBlockLength-1)/csumBlockLength )
63   32   fileSHA256Sum (only valid if
64                       blocks == (fileSize+csumBlockLength-1)/csumBlockLength )
65   followed by n entries:
66     16   md5sum of block of size csumBlockLength
67     32   sha256sum of block of size csumBlockLength
68
69   If stored csumBlockLength doesn't match supplied length, do nothing.
70   Otherwise, restore *this from cached data and return cached
71   blockLength (0 if not cached). The caller needs to make sure the
72   blockLength matches.
73
74   This is not a standard unserialize() member of FilePart because it
75   does not create a complete serialization - e.g. the location path
76   iter is missing. It only creates a cache entry. */
77
78 size_t FilePart::unserializeCacheEntry(const byte* data, size_t dataSize,
79                                        size_t csumBlockLength){
80   Assert(dataSize > PART_MD5SUMS);
81
82   // The resize() must have been made by the caller
83   Paranoid(MD5sums.size() == (size() + csumBlockLength - 1) / csumBlockLength);
84   Paranoid(SHA256sums.size() == (size() + csumBlockLength - 1) / csumBlockLength);
85
86   size_t cachedBlockLength;
87   data = unserialize4(cachedBlockLength, data);
88   size_t cachedCsumBlockLength;
89   data = unserialize4(cachedCsumBlockLength, data);
90   if (cachedCsumBlockLength != csumBlockLength) return 0;
91
92   size_t blocks;
93   data = unserialize4(blocks, data);
94   // Ignore strange-looking entries
95   if (blocks == 0) {
96     debug("ERR #blocks == 0");
97     return 0;
98   }
99   if (dataSize - PART_MD5SUMS != (blocks * CSUM_SIZE)) {
100     debug("ERR wrong entry size (%1 vs %2)",
101           blocks * CSUM_SIZE, dataSize - PART_MD5SUMS);
102     return 0;
103   }
104   Paranoid(serialSizeOf(rsyncSum) == 8);
105   data = unserialize(rsyncSum, data);
106   Paranoid(serialSizeOf(md5Sum) == 16);
107   Paranoid(serialSizeOf(sha256Sum) == 32);
108   // All blocks of file present?
109   if (blocks == MD5sums.size() + SHA256sums.size()) {
110     setFlag(MD_VALID);
111     data = unserialize(md5Sum, data);
112     data = unserialize(sha256Sum, data);
113   } else {
114     clearFlag(MD_VALID);
115     data += CSUM_SIZE;
116   }
117   // Read md5sums of individual chunks of file
118   vector<MD5>::iterator sum = MD5sums.begin();
119   for (size_t i = blocks; i > 0; --i) {
120     data = unserialize(*sum, data);
121     ++sum;
122   }
123   vector<SHA256>::iterator sum2 = SHA256sums.begin();
124   for (size_t i = blocks; i > 0; --i) {
125     data = unserialize(*sum2, data);
126     ++sum2;
127   }
128
129   return cachedBlockLength;
130 }
131 #endif
132 //______________________________________________________________________
133
134 #if HAVE_LIBDB
135 /** Opposite of unserializeCacheEntry; create byte stream from object */
136 struct FilePart::SerializeCacheEntry {
137   SerializeCacheEntry(const FilePart& f, JigdoCache* c, size_t blockLen,
138                       size_t md5Len)
139     : file(f), cache(c), blockLength(blockLen), csumBlockLength(md5Len) { }
140   const FilePart& file;
141   JigdoCache* cache;
142   size_t blockLength;
143   size_t csumBlockLength;
144
145   size_t serialSizeOf() {
146     return PART_MD5SUMS + (file.mdValid() ? file.MD5sums.size() * CSUM_SIZE : CSUM_SIZE);
147   }
148
149   void operator()(byte* data) {
150     Paranoid(file.getFlag(TO_BE_WRITTEN));
151     // If empty(), shouldn't have been marked TO_BE_WRITTEN:
152     Assert(!file.MD5sums.empty());
153     Assert(!file.SHA256sums.empty());
154
155     data = serialize4(blockLength, data);
156     data = serialize4(csumBlockLength, data);
157     // Nr of valid blocks - either 1 or all
158     size_t blocks = (file.mdValid() ? file.MD5sums.size() : 1);
159     data = serialize4(blocks, data);
160     data = serialize(file.rsyncSum, data);
161     data = serialize(file.md5Sum, data);
162     data = serialize(file.sha256Sum, data);
163     // Write md5sums of individual chunks of file
164     vector<MD5>::const_iterator sum = file.MD5sums.begin();
165     for (size_t i = blocks; i > 0; --i) {
166       data = serialize(*sum, data);
167       ++sum;
168     }
169     // Write md5sums of individual chunks of file
170     vector<SHA256>::const_iterator sum2 = file.SHA256sums.begin();
171     for (size_t i = blocks; i > 0; --i) {
172       data = serialize(*sum2, data);
173       ++sum2;
174     }
175   }
176 };
177 #endif
178 //______________________________________________________________________
179
180 #if HAVE_LIBDB
181 JigdoCache::JigdoCache(const string& cacheFileName, size_t expiryInSeconds,
182                        size_t bufLen, ProgressReporter& pr)
183   : blockLength(0), csumBlockLength(0), checkFiles(true), files(), nrOfFiles(0),
184     locationPaths(), readAmount(bufLen), buffer(), reporter(pr),
185     cacheExpiry(expiryInSeconds) {
186   cacheFile = 0;
187   try {
188     if (!cacheFileName.empty())
189       cacheFile = new CacheFile(cacheFileName.c_str());
190   } catch (DbError e) {
191     string err = subst(_("Could not open cache file: %L1"), e.message);
192     reporter.error(err);
193   }
194 }
195 #else
196 JigdoCache::JigdoCache(const string&, size_t, size_t bufLen,
197                        ProgressReporter& pr)
198   : blockLength(0), csumBlockLength(0), files(), nrOfFiles(0),
199     locationPaths(), readAmount(bufLen), buffer(), reporter(pr) { }
200 #endif
201 //______________________________________________________________________
202
203 JigdoCache::~JigdoCache() {
204 # if HAVE_LIBDB
205   if (cacheFile) {
206     // Write out any cache entries that need it
207     for (list<FilePart>::const_iterator i = files.begin(), e = files.end();
208          i != e; ++i) {
209       if (i->deleted() || !i->getFlag(FilePart::TO_BE_WRITTEN)) continue;
210       debug("Writing %1", i->leafName());
211       FilePart::SerializeCacheEntry serializer(*i, this, blockLength,
212                                                csumBlockLength);
213       try {
214         cacheFile->insert(serializer, serializer.serialSizeOf(),
215                           i->leafName(), i->mtime(), i->size());
216       } catch (DbError e) {
217         reporter.error(e.message);
218       }
219     }
220
221     if (cacheExpiry > 0) {
222       // Expire old cache entries from cache
223       time_t expired = time(0);
224       Paranoid(expired != static_cast<time_t>(-1));
225       expired -= cacheExpiry;
226       try {
227         cacheFile->expire(expired);
228       } catch (DbError e) {
229         string err = subst(_("Error during cache expiry: %1. The cache "
230                              "file may be corrupt, consider deleting it."),
231                            e.message);
232         reporter.error(err);
233       }
234     }
235
236     // Close db object, flushing changes to disc
237     delete cacheFile;
238   }
239 # endif
240 }
241 //______________________________________________________________________
242
243 /* Either:
244    
245    1. read data for the first block and create rsyncSum, MD5sums[0]
246       SHA256sums[0], or;
247
248    2. read the whole file and create rsyncSum, plus both checksums for
249       all the blocks and both checksums for the whole file.
250 */
251
252 bool FilePart::getChecksumsRead(JigdoCache* c, size_t blockNr) {
253   // Should do this check before calling:
254   Paranoid((blockNr == 0 && MD5sums.empty() && SHA256sums.empty()) || !mdValid());
255
256   // Do not forget to setParams() before calling this!
257   Assert(c->csumBlockLength != 0);
258   const size_t thisBlockLength = c->blockLength;
259
260   int64_t num_csum_blocks = (size() + c->csumBlockLength - 1) / c->csumBlockLength;
261
262   MD5sums.resize((size_t)num_csum_blocks);
263   SHA256sums.resize((size_t)num_csum_blocks);
264   //____________________
265
266 # if HAVE_LIBDB
267   // Can we maybe get the info from the cache?
268   if (c->cacheFile != 0 && !getFlag(WAS_LOOKED_UP)) {
269     setFlag(WAS_LOOKED_UP);
270     const byte* data;
271     size_t dataSize;
272     try {
273       /* Unserialize will do nothing if csumBlockLength differs. If
274          csumBlockLength matches, but returned blockLength doesn't, we
275          need to re-read the first block. */
276       if (c->cacheFile->find(data, dataSize, leafName(), size(), mtime())
277           .ok()) {
278         debug("%1 found, want block#%2", leafName(), blockNr);
279         size_t cachedBlockLength = unserializeCacheEntry(data, dataSize,
280                                                          c->csumBlockLength);
281         // Was all necessary data in cache? Yes => return it now.
282         if (cachedBlockLength == thisBlockLength
283             && (blockNr == 0 || mdValid())) {
284           debug("%1 loaded, blockLen (%2) matched, %3/%4 in cache",
285                 leafName(), thisBlockLength, (mdValid() ? MD5sums.size() : 1),
286                 MD5sums.size());
287           return true;
288         }
289         /* blockLengths didn't match and/or the cache only contained
290            the checksum for the first block while we asked for a later
291            one. It's as if we never queried the cache, except for the
292            case when we need to re-read the first block because the
293            blockLength changed, but *all* blocks' checksums were in the
294            cache. */
295         debug("%1 loaded, NO match (blockLen %2 vs %3), %4/%5 in cache",
296               leafName(), cachedBlockLength, thisBlockLength,
297               (mdValid() ? MD5sums.size() : 1), MD5sums.size());
298       }
299     } catch (DbError e) {
300       string err = subst(_("Error accessing cache: %1"), e.message);
301       c->reporter.error(err);
302     }
303   }
304 # endif /* HAVE_LIBDB */
305   //____________________
306
307   // Open input file
308   string name(getPath());
309   name += leafName();
310   bifstream input(name.c_str(), ios::binary);
311   if (!input) {
312     string err;
313     if (name == "-") {
314       /* Actually, stdin /would/ be allowed /here/, but it isn't
315          possible with mktemplate. */
316       err = _("Error opening file `-' "
317               "(using standard input not allowed here)");
318     } else {
319       err = subst(_("Could not open `%L1' for input - excluded"), name);
320       if (errno != 0) {
321         err += " (";
322         err += strerror(errno);
323         err += ')';
324       }
325     }
326     markAsDeleted(c);
327     c->reporter.error(err); // might throw
328     return 0;
329   }
330   //____________________
331
332   // We're going to write this to the cache later on
333   setFlag(TO_BE_WRITTEN);
334
335   // Allocate or resize buffer, or do nothing if already right size
336   c->buffer.resize(c->readAmount > c->csumBlockLength ?
337                    c->readAmount : c->csumBlockLength);
338   //______________________________
339
340   // Read data and create checksums
341
342   uint64 off = 0; // File offset of first byte in buf
343   // Nr of bytes before we are to reset() md
344   size_t mdLeft = c->csumBlockLength;
345   /* Call reporter once off reaches this value - only report something
346      if scanning >1 checksum block */
347   uint64 nextReport = mdLeft;
348   MD5Sum md;
349   md5Sum.reset();
350   vector<MD5>::iterator sum = MD5sums.begin();
351   SHA256Sum sd;
352   sha256Sum.reset();
353   vector<SHA256>::iterator sum2 = SHA256sums.begin();
354   //____________________
355
356   // Calculate RsyncSum of head of file and MD5 and SHA256 for all blocks
357
358   Assert(thisBlockLength <= c->csumBlockLength);
359   byte* buf = &c->buffer[0];
360   byte* bufpos = buf;
361   byte* bufend = buf + (c->readAmount > thisBlockLength ?
362                         c->readAmount : thisBlockLength);
363   while (input && static_cast<size_t>(bufpos - buf) < thisBlockLength) {
364     readBytes(input, bufpos, bufend - bufpos);
365     size_t nn = input.gcount();
366     bufpos += nn;
367     debug("Read %1", nn);
368   }
369   size_t n = bufpos - buf;
370
371   // Create RsyncSum of 1st bytes of file, or leave at 0 if file too small
372   rsyncSum.reset();
373   if (n >= thisBlockLength)
374     rsyncSum.addBack(buf, thisBlockLength);
375   //__________
376
377   while (true) { // Will break out if error or whole file read
378
379     // n is number of valid bytes in buf[]
380     off += n;
381     if (off > size())
382       break; // Argh - file size changed
383
384     if (off >= nextReport) {
385       c->reporter.scanningFile(this, off);
386       nextReport += REPORT_INTERVAL;
387     }
388
389     // Create checksums for chunks of size csumBlockLength
390     if (n < mdLeft) {
391       md.update(buf, n);
392       sd.update(buf, n);
393       mdLeft -= n;
394     } else {
395       md.update(buf, mdLeft);
396       sd.update(buf, mdLeft);
397       byte* cur = buf + mdLeft;
398       size_t nn = n - mdLeft;
399       do {
400         md.finishForReuse();
401         sd.finishForReuse();
402         debug("%1: mdLeft (0), switching to next md at off %2, left %3, "
403               "writing sum#%4: %5/%6", name, off - n + cur - buf, nn,
404               sum - MD5sums.begin(), md.toString(), sd.toString());
405         Paranoid(sum != MD5sums.end());
406         *sum = md;
407         *sum2 = sd;
408         ++sum;
409         ++sum2;
410         size_t m = (nn < c->csumBlockLength ? nn : c->csumBlockLength);
411         md.reset().update(cur, m);
412         sd.reset().update(cur, m);
413         cur += m;
414         nn -= m;
415         mdLeft = c->csumBlockLength - m;
416       } while (nn > 0);
417     }
418
419     md5Sum.update(buf, n); // Create MD5 for the whole file
420     sha256Sum.update(buf, n); // Create MD5 for the whole file
421
422     if (blockNr == 0 && sum != MD5sums.begin())
423       break; // Only wanted 1st block
424
425     if (!input)
426       break; // End of file or error
427
428     // Read more data
429     readBytes(input, buf, c->readAmount);
430     n = input.gcount();
431     debug("%1: read %2", name, n);
432
433   } // Endwhile (true), will break out if error or whole file read
434
435   Paranoid(sum != MD5sums.end() // >=1 trailing bytes
436            || mdLeft == c->csumBlockLength); // 0 trailing bytes
437   if (off == size() && input.eof()) {
438     // Whole file was read
439     c->reporter.scanningFile(this, size()); // 100% scanned
440     if (mdLeft < c->csumBlockLength) {
441       (*sum) = md.finish(); // Digest of trailing bytes
442       (*sum2) = sd.finish(); // Digest of trailing bytes
443       debug("%1: writing trailing sum#%2: %3/%4",
444             name, sum - MD5sums.begin(), md.toString(), sd.toString());
445     }
446     md5Sum.finish(); // Digest of whole file
447     sha256Sum.finish(); // Digest of whole file
448     setFlag(MD_VALID);
449     return true;
450   } else if (blockNr == 0 && sum != MD5sums.begin()) {
451     // Only first md5 block of file was read
452     debug("%1: file header read, sum#0 written", name);
453 #   if DEBUG
454     md5Sum.finish(); // else failed assert in FilePart::SerializeCacheEntry
455     sha256Sum.finish();
456 #   else
457     md5Sum.abort(); // Saves the memory until whole file is read
458     sha256Sum.abort(); // Saves the memory until whole file is read
459 #   endif
460     return true;
461   }
462   //____________________
463
464   // Some error happened
465   string err = subst(_("Error while reading `%1' - file will be ignored "
466                        "(%2)"), name, strerror(errno));
467   markAsDeleted(c);
468   c->reporter.error(err);
469   return 0;
470 }
471 //______________________________________________________________________
472
473 const MD5Sum* FilePart::getMD5SumRead(JigdoCache* c) {
474   if (!getChecksumsRead(c, (size_t)((fileSize + c->csumBlockLength - 1) / c->csumBlockLength - 1)))
475       return 0;
476   Paranoid(mdValid());
477   return &md5Sum;
478 }
479 //______________________________________________________________________
480
481 const SHA256Sum* FilePart::getSHA256SumRead(JigdoCache* c) {
482   if (!getChecksumsRead(c, (size_t)(fileSize + c->csumBlockLength - 1) / c->csumBlockLength - 1))
483       return 0;
484   Paranoid(mdValid());
485   return &sha256Sum;
486 }
487 //______________________________________________________________________
488
489 void JigdoCache::setParams(size_t blockLen, size_t csumBlockLen) {
490   if (blockLen == blockLength && csumBlockLen == csumBlockLength) return;
491
492   blockLength = blockLen;
493   csumBlockLength = csumBlockLen;
494   Assert(blockLength <= csumBlockLength);
495   for (list<FilePart>::iterator file = files.begin(), end = files.end();
496        file != end; ++file) {
497     file->MD5sums.resize(0);
498     file->SHA256sums.resize(0);
499   }
500 }
501 //______________________________________________________________________
502
503 void JigdoCache::addFile(const string& name) {
504   // Do not forget to setParams() before calling this!
505   Assert(csumBlockLength != 0);
506   // Assumes nonempty filenames
507   Paranoid(name.length() > 0);
508
509   // Find "//" in path and if present split name there
510   string::size_type pathLen = name.rfind(SPLITSEP);
511   string fileUri("file:");
512   string path, nameRest;
513   if (pathLen == string::npos) {
514     size_t splitAfter = 0;
515 #   if WINDOWS
516     // Split after "\" or ".\" or "C:" or "C:\" or "C:.\"
517     if (name.length() > 1 && isalpha(name[0]) && name[1] == ':')
518       splitAfter = 2;
519     if (name.length() > splitAfter && name[splitAfter] == '\\') {
520       // If an absolute path, split after leading '\'
521       ++splitAfter;
522     } else if (name.length() > splitAfter + 1
523                && name[splitAfter] == '.' && name[splitAfter + 1] == '\\') {
524       // Otherwise, also split after ".\" at start
525       splitAfter += 2;
526     }
527 #   else
528     // If an absolute path, split after leading '/'
529     if (name[0] == DIRSEP) splitAfter = 1;
530 #   endif
531     path.assign(name, 0, splitAfter);
532     fileUri += path;
533     nameRest.assign(name, splitAfter, string::npos);
534   } else {
535     // e.g. for name = "dir//file"
536     path.assign(name, 0, pathLen + 1); // path = "dir/"
537     fileUri.append(name, 0, pathLen + 1); // fileUri = "file:dir/"
538     // nameRest = "file"
539     nameRest.assign(name, pathLen + sizeof(SPLITSEP) - 1, string::npos);
540   }
541   compat_swapFileUriChars(fileUri); // Directory separator is always '/'
542   ConfigFile::quote(fileUri);
543   //____________________
544
545   // If necessary, create a label for the path before "//"
546   static string emptylabel;
547   LocationPath tmp(path, emptylabel, fileUri);
548   LocationPathSet::iterator i = locationPaths.find(tmp);
549   if (i == locationPaths.end())
550     i = locationPaths.insert(tmp).first; // Any new entry has a "" label
551   Paranoid(i != locationPaths.end());
552
553   // Append new obj at end of list
554   FilePart fp(i, nameRest, fileInfo.st_size, fileInfo.st_mtime);
555   files.push_back(fp);
556 }