]> git.saurik.com Git - apt.git/blob - methods/rred.cc
rewrite and refactor rred method to be able to handle even big (>30 MB)
[apt.git] / methods / rred.cc
1 // Includes /*{{{*/
2 #include <apt-pkg/fileutl.h>
3 #include <apt-pkg/error.h>
4 #include <apt-pkg/acquire-method.h>
5 #include <apt-pkg/strutl.h>
6 #include <apt-pkg/hashes.h>
7
8 #include <sys/stat.h>
9 #include <unistd.h>
10 #include <utime.h>
11 #include <stdio.h>
12 #include <errno.h>
13 #include <apti18n.h>
14 /*}}}*/
15 /** \brief RredMethod - ed-style incremential patch method {{{
16 *
17 * This method implements a patch functionality similar to "patch --ed" that is
18 * used by the "tiffany" incremental packages download stuff. It differs from
19 * "ed" insofar that it is way more restricted (and therefore secure).
20 * The currently supported ed commands are "<em>c</em>hange", "<em>a</em>dd" and
21 * "<em>d</em>elete" (diff doesn't output any other).
22 * Additionally the records must be reverse sorted by line number and
23 * may not overlap (diff *seems* to produce this kind of output).
24 * */
25 class RredMethod : public pkgAcqMethod {
26 bool Debug;
27 // the size of this doesn't really matter (except for performance)
28 const static int BUF_SIZE = 1024;
29 // the supported ed commands
30 enum Mode {MODE_CHANGED='c', MODE_DELETED='d', MODE_ADDED='a'};
31 // return values
32 enum State {ED_OK=0, ED_ORDERING=1, ED_PARSER=2, ED_FAILURE=3};
33
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 copyLineFromFileToFile(FILE *fin, FILE *fout,
38 Hashes *hash, char *buffer) const;
39
40 State patchFile(FILE *ed_cmds, FILE *in_file, FILE *out_file, Hashes *hash) const;
41
42 protected:
43 // the methods main method
44 virtual bool Fetch(FetchItem *Itm);
45
46 public:
47 RredMethod() : pkgAcqMethod("1.1",SingleInstance | SendConfig) {};
48 };
49 /*}}}*/
50 /** \brief applyFile - in reverse order with a tail recursion {{{
51 *
52 * As it is expected that the commands are in reversed order in the patch file
53 * we check in the first half if the command is valid, but doesn't execute it
54 * and move a step deeper. After reaching the end of the file we apply the
55 * patches in the correct order: last found command first.
56 *
57 * \param ed_cmds patch file to apply
58 * \param in_file base file we want to patch
59 * \param out_file file to write the patched result to
60 * \param line of command operation
61 * \param buffer internal used read/write buffer
62 * \param hash the created file for correctness
63 * \return the success State of the ed command executor
64 */
65 RredMethod::State RredMethod::applyFile(FILE *ed_cmds, FILE *in_file, FILE *out_file,
66 unsigned long &line, char *buffer, Hashes *hash) const {
67 // get the current command and parse it
68 if (fgets(buffer, BUF_SIZE, ed_cmds) == NULL) {
69 if (Debug == true)
70 std::clog << "rred: encounter end of file - we can start patching now.";
71 line = 0;
72 return ED_OK;
73 }
74
75 // parse in the effected linenumbers
76 char* idx;
77 errno=0;
78 unsigned long const startline = strtol(buffer, &idx, 10);
79 if (errno == ERANGE || errno == EINVAL) {
80 _error->Errno("rred", "startline is an invalid number");
81 return ED_PARSER;
82 }
83 if (startline > line) {
84 _error->Error("rred: The start line (%lu) of the next command is higher than the last line (%lu). This is not allowed.", startline, line);
85 return ED_ORDERING;
86 }
87 unsigned long stopline;
88 if (*idx == ',') {
89 idx++;
90 errno=0;
91 stopline = strtol(idx, &idx, 10);
92 if (errno == ERANGE || errno == EINVAL) {
93 _error->Errno("rred", "stopline is an invalid number");
94 return ED_PARSER;
95 }
96 }
97 else {
98 stopline = startline;
99 }
100 line = startline;
101
102 // which command to execute on this line(s)?
103 switch (*idx) {
104 case MODE_CHANGED:
105 if (Debug == true)
106 std::clog << "Change from line " << startline << " to " << stopline << std::endl;
107 break;
108 case MODE_ADDED:
109 if (Debug == true)
110 std::clog << "Insert after line " << startline << std::endl;
111 break;
112 case MODE_DELETED:
113 if (Debug == true)
114 std::clog << "Delete from line " << startline << " to " << stopline << std::endl;
115 break;
116 default:
117 _error->Error("rred: Unknown ed command '%c'. Abort.", *idx);
118 return ED_PARSER;
119 }
120 unsigned char mode = *idx;
121
122 // save the current position
123 unsigned const long pos = ftell(ed_cmds);
124
125 // if this is add or change then go to the next full stop
126 if (mode == MODE_CHANGED || mode == MODE_ADDED) {
127 do
128 ignoreLineInFile(ed_cmds, buffer);
129 while (strncmp(buffer, ".", 1) != 0);
130 }
131
132 // do the recursive call - the last command is the one we need to execute at first
133 const State child = applyFile(ed_cmds, in_file, out_file, line, buffer, hash);
134 if (child != ED_OK) {
135 return child;
136 }
137
138 // change and delete are working on "line" - add is done after "line"
139 if (mode != MODE_ADDED)
140 line++;
141
142 // first wind to the current position and copy over all unchanged lines
143 while (line < startline) {
144 fgets(buffer, BUF_SIZE, in_file);
145 copyLineFromFileToFile(in_file, out_file, hash, buffer);
146 line++;
147 }
148
149 if (mode != MODE_ADDED)
150 line--;
151
152 // include data from ed script
153 if (mode == MODE_CHANGED || mode == MODE_ADDED) {
154 fseek(ed_cmds, pos, SEEK_SET);
155 while(fgets(buffer, BUF_SIZE, ed_cmds) != NULL) {
156 if (strncmp(buffer, ".", 1) != 0)
157 copyLineFromFileToFile(ed_cmds, out_file, hash, buffer);
158 else
159 break;
160 }
161 }
162
163 // ignore the corresponding number of lines from input
164 if (mode == MODE_CHANGED || mode == MODE_DELETED) {
165 while (line < stopline) {
166 ignoreLineInFile(in_file, buffer);
167 line++;
168 }
169 }
170 return ED_OK;
171 }
172 /*}}}*/
173 void RredMethod::copyLineFromFileToFile(FILE *fin, FILE *fout, /*{{{*/
174 Hashes *hash, char *buffer) const {
175 size_t written = fwrite(buffer, 1, strlen(buffer), fout);
176 hash->Add((unsigned char*)buffer, written);
177 while (strlen(buffer) == (BUF_SIZE - 1) &&
178 buffer[BUF_SIZE - 2] != '\n') {
179 fgets(buffer, BUF_SIZE, fin);
180 written = fwrite(buffer, 1, strlen(buffer), fout);
181 hash->Add((unsigned char*)buffer, written);
182 }
183 }
184 /*}}}*/
185 void RredMethod::ignoreLineInFile(FILE *fin, char *buffer) const { /*{{{*/
186 fgets(buffer, BUF_SIZE, fin);
187 while (strlen(buffer) == (BUF_SIZE - 1) &&
188 buffer[BUF_SIZE - 2] != '\n') {
189 fgets(buffer, BUF_SIZE, fin);
190 buffer[0] = ' ';
191 }
192 }
193 /*}}}*/
194 RredMethod::State RredMethod::patchFile(FILE *ed_cmds, FILE *in_file, FILE *out_file, /*{{{*/
195 Hashes *hash) const {
196 char buffer[BUF_SIZE];
197
198 /* we do a tail recursion to read the commands in the right order */
199 unsigned long line = -1; // assign highest possible value
200 State result = applyFile(ed_cmds, in_file, out_file, line, buffer, hash);
201
202 /* read the rest from infile */
203 if (result == ED_OK) {
204 while (fgets(buffer, BUF_SIZE, in_file) != NULL) {
205 size_t const written = fwrite(buffer, 1, strlen(buffer), out_file);
206 hash->Add((unsigned char*)buffer, written);
207 }
208 }
209 return result;
210 }
211 /*}}}*/
212 bool RredMethod::Fetch(FetchItem *Itm) /*{{{*/
213 {
214 Debug = _config->FindB("Debug::pkgAcquire::RRed", false);
215 URI Get = Itm->Uri;
216 string Path = Get.Host + Get.Path; // To account for relative paths
217
218 FetchResult Res;
219 Res.Filename = Itm->DestFile;
220 if (Itm->Uri.empty() == true) {
221 Path = Itm->DestFile;
222 Itm->DestFile.append(".result");
223 } else
224 URIStart(Res);
225
226 if (Debug == true)
227 std::clog << "Patching " << Path << " with " << Path
228 << ".ed and putting result into " << Itm->DestFile << std::endl;
229 // Open the source and destination files (the d'tor of FileFd will do
230 // the cleanup/closing of the fds)
231 FileFd From(Path,FileFd::ReadOnly);
232 FileFd Patch(Path+".ed",FileFd::ReadOnly);
233 FileFd To(Itm->DestFile,FileFd::WriteEmpty);
234 To.EraseOnFailure();
235 if (_error->PendingError() == true)
236 return false;
237
238 Hashes Hash;
239 FILE* fFrom = fdopen(From.Fd(), "r");
240 FILE* fPatch = fdopen(Patch.Fd(), "r");
241 FILE* fTo = fdopen(To.Fd(), "w");
242 // now do the actual patching
243 if (patchFile(fPatch, fFrom, fTo, &Hash) != ED_OK) {
244 _error->Errno("rred", _("Could not patch file"));
245 return false;
246 }
247
248 // write out the result
249 fflush(fFrom);
250 fflush(fPatch);
251 fflush(fTo);
252 From.Close();
253 Patch.Close();
254 To.Close();
255
256 // Transfer the modification times
257 struct stat Buf;
258 if (stat(Path.c_str(),&Buf) != 0)
259 return _error->Errno("stat",_("Failed to stat"));
260
261 struct utimbuf TimeBuf;
262 TimeBuf.actime = Buf.st_atime;
263 TimeBuf.modtime = Buf.st_mtime;
264 if (utime(Itm->DestFile.c_str(),&TimeBuf) != 0)
265 return _error->Errno("utime",_("Failed to set modification time"));
266
267 if (stat(Itm->DestFile.c_str(),&Buf) != 0)
268 return _error->Errno("stat",_("Failed to stat"));
269
270 // return done
271 if (Itm->Uri.empty() == true) {
272 Res.LastModified = Buf.st_mtime;
273 Res.Size = Buf.st_size;
274 Res.TakeHashes(Hash);
275 URIDone(Res);
276 }
277
278 return true;
279 }
280 /*}}}*/
281 /** \brief Wrapper class for testing rred */ /*{{{*/
282 class TestRredMethod : public RredMethod {
283 public:
284 /** \brief Run rred in debug test mode
285 *
286 * This method can be used to run the rred method outside
287 * of the "normal" acquire environment for easier testing.
288 *
289 * \param base basename of all files involved in this rred test
290 */
291 bool Run(char const *base) {
292 _config->CndSet("Debug::pkgAcquire::RRed", "true");
293 FetchItem *test = new FetchItem;
294 test->DestFile = base;
295 return Fetch(test);
296 }
297 };
298 /*}}}*/
299 /** \brief Starter for the rred method (or its test method) {{{
300 *
301 * Used without parameters is the normal behavior for methods for
302 * the APT acquire system. While this works great for the acquire system
303 * it is very hard to test the method and therefore the method also
304 * accepts one parameter which will switch it directly to debug test mode:
305 * The test mode expects that if "Testfile" is given as parameter
306 * the file "Testfile" should be ed-style patched with "Testfile.ed"
307 * and will write the result to "Testfile.result".
308 */
309 int main(int argc, char *argv[]) {
310 if (argc == 0) {
311 RredMethod Mth;
312 return Mth.Run();
313 } else {
314 TestRredMethod Mth;
315 bool result = Mth.Run(argv[1]);
316 _error->DumpErrors();
317 return result;
318 }
319 }
320 /*}}}*/