copyLinesFromFileToFile instead of a single Line in the rred method
[ntk/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 copyLinesFromFileToFile(FILE *fin, FILE *fout, unsigned int lines,
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 unsigned int data_length = 0;
127 if (mode == MODE_CHANGED || mode == MODE_ADDED) {
128 do {
129 ignoreLineInFile(ed_cmds, buffer);
130 data_length++;
131 }
132 while (strncmp(buffer, ".", 1) != 0);
133 data_length--; // the dot should not be copied
134 }
135
136 // do the recursive call - the last command is the one we need to execute at first
137 const State child = applyFile(ed_cmds, in_file, out_file, line, buffer, hash);
138 if (child != ED_OK) {
139 return child;
140 }
141
142 // change and delete are working on "line" - add is done after "line"
143 if (mode != MODE_ADDED)
144 line++;
145
146 // first wind to the current position and copy over all unchanged lines
147 if (line < startline) {
148 copyLinesFromFileToFile(in_file, out_file, (startline - line), hash, buffer);
149 line = startline;
150 }
151
152 if (mode != MODE_ADDED)
153 line--;
154
155 // include data from ed script
156 if (mode == MODE_CHANGED || mode == MODE_ADDED) {
157 fseek(ed_cmds, pos, SEEK_SET);
158 copyLinesFromFileToFile(ed_cmds, out_file, data_length, hash, buffer);
159 }
160
161 // ignore the corresponding number of lines from input
162 if (mode == MODE_CHANGED || mode == MODE_DELETED) {
163 while (line < stopline) {
164 ignoreLineInFile(in_file, buffer);
165 line++;
166 }
167 }
168 return ED_OK;
169 }
170 /*}}}*/
171 void RredMethod::copyLinesFromFileToFile(FILE *fin, FILE *fout, unsigned int lines,/*{{{*/
172 Hashes *hash, char *buffer) const {
173 while (0 < lines--) {
174 do {
175 fgets(buffer, BUF_SIZE, fin);
176 size_t const written = fwrite(buffer, 1, strlen(buffer), fout);
177 hash->Add((unsigned char*)buffer, written);
178 } while (strlen(buffer) == (BUF_SIZE - 1) &&
179 buffer[BUF_SIZE - 2] != '\n');
180 }
181 }
182 /*}}}*/
183 void RredMethod::ignoreLineInFile(FILE *fin, char *buffer) const { /*{{{*/
184 fgets(buffer, BUF_SIZE, fin);
185 while (strlen(buffer) == (BUF_SIZE - 1) &&
186 buffer[BUF_SIZE - 2] != '\n') {
187 fgets(buffer, BUF_SIZE, fin);
188 buffer[0] = ' ';
189 }
190 }
191 /*}}}*/
192 RredMethod::State RredMethod::patchFile(FILE *ed_cmds, FILE *in_file, FILE *out_file, /*{{{*/
193 Hashes *hash) const {
194 char buffer[BUF_SIZE];
195
196 /* we do a tail recursion to read the commands in the right order */
197 unsigned long line = -1; // assign highest possible value
198 State result = applyFile(ed_cmds, in_file, out_file, line, buffer, hash);
199
200 /* read the rest from infile */
201 if (result == ED_OK) {
202 while (fgets(buffer, BUF_SIZE, in_file) != NULL) {
203 size_t const written = fwrite(buffer, 1, strlen(buffer), out_file);
204 hash->Add((unsigned char*)buffer, written);
205 }
206 }
207 return result;
208 }
209 /*}}}*/
210 bool RredMethod::Fetch(FetchItem *Itm) /*{{{*/
211 {
212 Debug = _config->FindB("Debug::pkgAcquire::RRed", false);
213 URI Get = Itm->Uri;
214 string Path = Get.Host + Get.Path; // To account for relative paths
215
216 FetchResult Res;
217 Res.Filename = Itm->DestFile;
218 if (Itm->Uri.empty() == true) {
219 Path = Itm->DestFile;
220 Itm->DestFile.append(".result");
221 } else
222 URIStart(Res);
223
224 if (Debug == true)
225 std::clog << "Patching " << Path << " with " << Path
226 << ".ed and putting result into " << Itm->DestFile << std::endl;
227 // Open the source and destination files (the d'tor of FileFd will do
228 // the cleanup/closing of the fds)
229 FileFd From(Path,FileFd::ReadOnly);
230 FileFd Patch(Path+".ed",FileFd::ReadOnly);
231 FileFd To(Itm->DestFile,FileFd::WriteEmpty);
232 To.EraseOnFailure();
233 if (_error->PendingError() == true)
234 return false;
235
236 Hashes Hash;
237 FILE* fFrom = fdopen(From.Fd(), "r");
238 FILE* fPatch = fdopen(Patch.Fd(), "r");
239 FILE* fTo = fdopen(To.Fd(), "w");
240 // now do the actual patching
241 if (patchFile(fPatch, fFrom, fTo, &Hash) != ED_OK) {
242 _error->Errno("rred", _("Could not patch file"));
243 return false;
244 }
245
246 // write out the result
247 fflush(fFrom);
248 fflush(fPatch);
249 fflush(fTo);
250 From.Close();
251 Patch.Close();
252 To.Close();
253
254 // Transfer the modification times
255 struct stat Buf;
256 if (stat(Path.c_str(),&Buf) != 0)
257 return _error->Errno("stat",_("Failed to stat"));
258
259 struct utimbuf TimeBuf;
260 TimeBuf.actime = Buf.st_atime;
261 TimeBuf.modtime = Buf.st_mtime;
262 if (utime(Itm->DestFile.c_str(),&TimeBuf) != 0)
263 return _error->Errno("utime",_("Failed to set modification time"));
264
265 if (stat(Itm->DestFile.c_str(),&Buf) != 0)
266 return _error->Errno("stat",_("Failed to stat"));
267
268 // return done
269 if (Itm->Uri.empty() == true) {
270 Res.LastModified = Buf.st_mtime;
271 Res.Size = Buf.st_size;
272 Res.TakeHashes(Hash);
273 URIDone(Res);
274 }
275
276 return true;
277 }
278 /*}}}*/
279 /** \brief Wrapper class for testing rred */ /*{{{*/
280 class TestRredMethod : public RredMethod {
281 public:
282 /** \brief Run rred in debug test mode
283 *
284 * This method can be used to run the rred method outside
285 * of the "normal" acquire environment for easier testing.
286 *
287 * \param base basename of all files involved in this rred test
288 */
289 bool Run(char const *base) {
290 _config->CndSet("Debug::pkgAcquire::RRed", "true");
291 FetchItem *test = new FetchItem;
292 test->DestFile = base;
293 return Fetch(test);
294 }
295 };
296 /*}}}*/
297 /** \brief Starter for the rred method (or its test method) {{{
298 *
299 * Used without parameters is the normal behavior for methods for
300 * the APT acquire system. While this works great for the acquire system
301 * it is very hard to test the method and therefore the method also
302 * accepts one parameter which will switch it directly to debug test mode:
303 * The test mode expects that if "Testfile" is given as parameter
304 * the file "Testfile" should be ed-style patched with "Testfile.ed"
305 * and will write the result to "Testfile.result".
306 */
307 int main(int argc, char *argv[]) {
308 if (argc == 0) {
309 RredMethod Mth;
310 return Mth.Run();
311 } else {
312 TestRredMethod Mth;
313 bool result = Mth.Run(argv[1]);
314 _error->DumpErrors();
315 return result;
316 }
317 }
318 /*}}}*/