]>
Commit | Line | Data |
---|---|---|
b2e465d6 AL |
1 | // -*- mode: cpp; mode: fold -*- |
2 | // Description /*{{{*/ | |
b3d44315 | 3 | // $Id: extract.cc,v 1.6.2.1 2004/01/16 18:58:50 mdz Exp $ |
b2e465d6 AL |
4 | /* ###################################################################### |
5 | ||
6 | Archive Extraction Directory Stream | |
7 | ||
8 | Extraction for each file is a bit of an involved process. Each object | |
9 | undergoes an atomic backup, overwrite, erase sequence. First the | |
10 | object is unpacked to '.dpkg.new' then the original is hardlinked to | |
11 | '.dpkg.tmp' and finally the new object is renamed to overwrite the old | |
12 | one. From an external perspective the file never ceased to exist. | |
1e3f4083 | 13 | After the archive has been successfully unpacked the .dpkg.tmp files |
b2e465d6 AL |
14 | are erased. A failure causes all the .dpkg.tmp files to be restored. |
15 | ||
16 | Decisions about unpacking go like this: | |
17 | - Store the original filename in the file listing | |
18 | - Resolve any diversions that would effect this file, all checks | |
19 | below apply to the diverted name, not the real one. | |
20 | - Resolve any symlinked configuration files. | |
21 | - If the existing file does not exist then .dpkg-tmp is checked for. | |
22 | [Note, this is reduced to only check if a file was expected to be | |
23 | there] | |
24 | - If the existing link/file is not a directory then it is replaced | |
1e3f4083 | 25 | regardless |
b2e465d6 AL |
26 | - If the existing link/directory is being replaced by a directory then |
27 | absolutely nothing happens. | |
28 | - If the existing link/directory is being replaced by a link then | |
29 | absolutely nothing happens. | |
30 | - If the existing link/directory is being replaced by a non-directory | |
31 | then this will abort if the package is not the sole owner of the | |
32 | directory. [Note, this is changed to not happen if the directory | |
33 | non-empty - that is, it only includes files that are part of this | |
34 | package - prevents removing user files accidentally.] | |
35 | - If the non-directory exists in the listing database and it | |
36 | does not belong to the current package then an overwrite condition | |
37 | is invoked. | |
38 | ||
39 | As we unpack we record the file list differences in the FL cache. If | |
40 | we need to unroll the the FL cache knows which files have been unpacked | |
41 | and can undo. When we need to erase then it knows which files have not | |
42 | been unpacked. | |
43 | ||
44 | ##################################################################### */ | |
45 | /*}}}*/ | |
46 | // Include Files /*{{{*/ | |
ea542140 DK |
47 | #include<config.h> |
48 | ||
b2e465d6 AL |
49 | #include <apt-pkg/extract.h> |
50 | #include <apt-pkg/error.h> | |
51 | #include <apt-pkg/debversion.h> | |
472ff00e | 52 | #include <apt-pkg/fileutl.h> |
453b82a3 DK |
53 | #include <apt-pkg/dirstream.h> |
54 | #include <apt-pkg/filelist.h> | |
55 | #include <apt-pkg/mmap.h> | |
56 | #include <apt-pkg/pkgcache.h> | |
57 | #include <apt-pkg/cacheiterators.h> | |
b2e465d6 | 58 | |
453b82a3 DK |
59 | #include <string.h> |
60 | #include <string> | |
b2e465d6 AL |
61 | #include <sys/stat.h> |
62 | #include <stdio.h> | |
b2e465d6 AL |
63 | #include <errno.h> |
64 | #include <dirent.h> | |
90f057fd | 65 | #include <iostream> |
453b82a3 | 66 | |
d77559ac | 67 | #include <apti18n.h> |
b2e465d6 | 68 | /*}}}*/ |
584e4558 | 69 | using namespace std; |
b2e465d6 AL |
70 | |
71 | static const char *TempExt = "dpkg-tmp"; | |
72 | //static const char *NewExt = "dpkg-new"; | |
73 | ||
74 | // Extract::pkgExtract - Constructor /*{{{*/ | |
75 | // --------------------------------------------------------------------- | |
76 | /* */ | |
77 | pkgExtract::pkgExtract(pkgFLCache &FLCache,pkgCache::VerIterator Ver) : | |
78 | FLCache(FLCache), Ver(Ver) | |
79 | { | |
80 | FLPkg = FLCache.GetPkg(Ver.ParentPkg().Name(),true); | |
81 | if (FLPkg.end() == true) | |
82 | return; | |
83 | Debug = true; | |
84 | } | |
85 | /*}}}*/ | |
86 | // Extract::DoItem - Handle a single item from the stream /*{{{*/ | |
87 | // --------------------------------------------------------------------- | |
88 | /* This performs the setup for the extraction.. */ | |
65512241 | 89 | bool pkgExtract::DoItem(Item &Itm, int &/*Fd*/) |
b2e465d6 | 90 | { |
b2e465d6 AL |
91 | /* Strip any leading/trailing /s from the filename, then copy it to the |
92 | temp buffer and re-apply the leading / We use a class variable | |
93 | to store the new filename for use by the three extraction funcs */ | |
94 | char *End = FileName+1; | |
95 | const char *I = Itm.Name; | |
96 | for (; *I != 0 && *I == '/'; I++); | |
97 | *FileName = '/'; | |
98 | for (; *I != 0 && End < FileName + sizeof(FileName); I++, End++) | |
99 | *End = *I; | |
100 | if (End + 20 >= FileName + sizeof(FileName)) | |
05eb7df0 | 101 | return _error->Error(_("The path %s is too long"),Itm.Name); |
b2e465d6 AL |
102 | for (; End > FileName && End[-1] == '/'; End--); |
103 | *End = 0; | |
104 | Itm.Name = FileName; | |
105 | ||
106 | /* Lookup the file. Nde is the file [group] we are going to write to and | |
107 | RealNde is the actual node we are manipulating. Due to diversions | |
108 | they may be entirely different. */ | |
109 | pkgFLCache::NodeIterator Nde = FLCache.GetNode(Itm.Name,End,0,false,false); | |
110 | pkgFLCache::NodeIterator RealNde = Nde; | |
111 | ||
112 | // See if the file is already in the file listing | |
113 | unsigned long FileGroup = RealNde->File; | |
114 | for (; RealNde.end() == false && FileGroup == RealNde->File; RealNde++) | |
115 | if (RealNde.RealPackage() == FLPkg) | |
116 | break; | |
117 | ||
118 | // Nope, create an entry | |
119 | if (RealNde.end() == true) | |
120 | { | |
121 | RealNde = FLCache.GetNode(Itm.Name,End,FLPkg.Offset(),true,false); | |
122 | if (RealNde.end() == true) | |
123 | return false; | |
124 | RealNde->Flags |= pkgFLCache::Node::NewFile; | |
125 | } | |
126 | ||
127 | /* Check if this entry already was unpacked. The only time this should | |
128 | ever happen is if someone has hacked tar to support capabilities, in | |
129 | which case this needs to be modified anyhow.. */ | |
130 | if ((RealNde->Flags & pkgFLCache::Node::Unpacked) == | |
131 | pkgFLCache::Node::Unpacked) | |
05eb7df0 | 132 | return _error->Error(_("Unpacking %s more than once"),Itm.Name); |
b2e465d6 AL |
133 | |
134 | if (Nde.end() == true) | |
135 | Nde = RealNde; | |
136 | ||
137 | /* Consider a diverted file - We are not permitted to divert directories, | |
138 | but everything else is fair game (including conf files!) */ | |
139 | if ((Nde->Flags & pkgFLCache::Node::Diversion) != 0) | |
140 | { | |
141 | if (Itm.Type == Item::Directory) | |
05eb7df0 | 142 | return _error->Error(_("The directory %s is diverted"),Itm.Name); |
b2e465d6 AL |
143 | |
144 | /* A package overwriting a diversion target is just the same as | |
145 | overwriting a normally owned file and is checked for below in | |
146 | the overwrites mechanism */ | |
147 | ||
148 | /* If this package is trying to overwrite the target of a diversion, | |
149 | that is never, ever permitted */ | |
150 | pkgFLCache::DiverIterator Div = Nde.Diversion(); | |
151 | if (Div.DivertTo() == Nde) | |
05eb7df0 AL |
152 | return _error->Error(_("The package is trying to write to the " |
153 | "diversion target %s/%s"),Nde.DirN(),Nde.File()); | |
b2e465d6 AL |
154 | |
155 | // See if it is us and we are following it in the right direction | |
156 | if (Div->OwnerPkg != FLPkg.Offset() && Div.DivertFrom() == Nde) | |
157 | { | |
158 | Nde = Div.DivertTo(); | |
159 | End = FileName + snprintf(FileName,sizeof(FileName)-20,"%s/%s", | |
160 | Nde.DirN(),Nde.File()); | |
161 | if (End <= FileName) | |
05eb7df0 | 162 | return _error->Error(_("The diversion path is too long")); |
b2e465d6 AL |
163 | } |
164 | } | |
165 | ||
166 | // Deal with symlinks and conf files | |
167 | if ((RealNde->Flags & pkgFLCache::Node::NewConfFile) == | |
168 | pkgFLCache::Node::NewConfFile) | |
169 | { | |
170 | string Res = flNoLink(Itm.Name); | |
171 | if (Res.length() > sizeof(FileName)) | |
05eb7df0 | 172 | return _error->Error(_("The path %s is too long"),Res.c_str()); |
b2e465d6 AL |
173 | if (Debug == true) |
174 | clog << "Followed conf file from " << FileName << " to " << Res << endl; | |
175 | Itm.Name = strcpy(FileName,Res.c_str()); | |
176 | } | |
177 | ||
178 | /* Get information about the existing file, and attempt to restore | |
179 | a backup if it does not exist */ | |
180 | struct stat LExisting; | |
181 | bool EValid = false; | |
182 | if (lstat(Itm.Name,&LExisting) != 0) | |
183 | { | |
184 | // This is bad news. | |
185 | if (errno != ENOENT) | |
05eb7df0 | 186 | return _error->Errno("stat",_("Failed to stat %s"),Itm.Name); |
b2e465d6 AL |
187 | |
188 | // See if we can recover the backup file | |
189 | if (Nde.end() == false) | |
190 | { | |
69c2ecbd | 191 | char Temp[sizeof(FileName)]; |
b2e465d6 AL |
192 | snprintf(Temp,sizeof(Temp),"%s.%s",Itm.Name,TempExt); |
193 | if (rename(Temp,Itm.Name) != 0 && errno != ENOENT) | |
05eb7df0 | 194 | return _error->Errno("rename",_("Failed to rename %s to %s"), |
b2e465d6 AL |
195 | Temp,Itm.Name); |
196 | if (stat(Itm.Name,&LExisting) != 0) | |
197 | { | |
198 | if (errno != ENOENT) | |
05eb7df0 | 199 | return _error->Errno("stat",_("Failed to stat %s"),Itm.Name); |
b2e465d6 AL |
200 | } |
201 | else | |
202 | EValid = true; | |
203 | } | |
204 | } | |
205 | else | |
206 | EValid = true; | |
207 | ||
208 | /* If the file is a link we need to stat its destination, get the | |
209 | existing file modes */ | |
210 | struct stat Existing = LExisting; | |
211 | if (EValid == true && S_ISLNK(Existing.st_mode)) | |
212 | { | |
213 | if (stat(Itm.Name,&Existing) != 0) | |
214 | { | |
215 | if (errno != ENOENT) | |
05eb7df0 | 216 | return _error->Errno("stat",_("Failed to stat %s"),Itm.Name); |
b2e465d6 AL |
217 | Existing = LExisting; |
218 | } | |
219 | } | |
220 | ||
221 | // We pretend a non-existing file looks like it is a normal file | |
222 | if (EValid == false) | |
223 | Existing.st_mode = S_IFREG; | |
224 | ||
225 | /* Okay, at this point 'Existing' is the stat information for the | |
226 | real non-link file */ | |
227 | ||
228 | /* The only way this can be a no-op is if a directory is being | |
229 | replaced by a directory or by a link */ | |
230 | if (S_ISDIR(Existing.st_mode) != 0 && | |
231 | (Itm.Type == Item::Directory || Itm.Type == Item::SymbolicLink)) | |
232 | return true; | |
233 | ||
234 | /* Non-Directory being replaced by non-directory. We check for over | |
235 | writes here. */ | |
236 | if (Nde.end() == false) | |
237 | { | |
238 | if (HandleOverwrites(Nde) == false) | |
239 | return false; | |
240 | } | |
241 | ||
242 | /* Directory being replaced by a non-directory - this needs to see if | |
243 | the package is the owner and then see if the directory would be | |
244 | empty after the package is removed [ie no user files will be | |
245 | erased] */ | |
246 | if (S_ISDIR(Existing.st_mode) != 0) | |
247 | { | |
248 | if (CheckDirReplace(Itm.Name) == false) | |
05eb7df0 | 249 | return _error->Error(_("The directory %s is being replaced by a non-directory"),Itm.Name); |
b2e465d6 AL |
250 | } |
251 | ||
252 | if (Debug == true) | |
253 | clog << "Extract " << string(Itm.Name,End) << endl; | |
254 | /* if (Count != 0) | |
05eb7df0 | 255 | return _error->Error(_("Done"));*/ |
b2e465d6 AL |
256 | |
257 | return true; | |
258 | } | |
259 | /*}}}*/ | |
260 | // Extract::Finished - Sequence finished, erase the temp files /*{{{*/ | |
261 | // --------------------------------------------------------------------- | |
262 | /* */ | |
a02db58f | 263 | APT_CONST bool pkgExtract::Finished() |
b2e465d6 AL |
264 | { |
265 | return true; | |
266 | } | |
267 | /*}}}*/ | |
268 | // Extract::Aborted - Sequence aborted, undo all our unpacking /*{{{*/ | |
269 | // --------------------------------------------------------------------- | |
270 | /* This undoes everything that was done by all calls to the DoItem method | |
271 | and restores the File Listing cache to its original form. It bases its | |
272 | actions on the flags value for each node in the cache. */ | |
273 | bool pkgExtract::Aborted() | |
274 | { | |
275 | if (Debug == true) | |
276 | clog << "Aborted, backing out" << endl; | |
277 | ||
278 | pkgFLCache::NodeIterator Files = FLPkg.Files(); | |
279 | map_ptrloc *Last = &FLPkg->Files; | |
280 | ||
281 | /* Loop over all files, restore those that have been unpacked from their | |
8d89cda7 | 282 | dpkg-tmp entries */ |
b2e465d6 AL |
283 | while (Files.end() == false) |
284 | { | |
285 | // Locate the hash bucket for the node and locate its group head | |
286 | pkgFLCache::NodeIterator Nde(FLCache,FLCache.HashNode(Files)); | |
287 | for (; Nde.end() == false && Files->File != Nde->File; Nde++); | |
288 | if (Nde.end() == true) | |
05eb7df0 | 289 | return _error->Error(_("Failed to locate node in its hash bucket")); |
b2e465d6 AL |
290 | |
291 | if (snprintf(FileName,sizeof(FileName)-20,"%s/%s", | |
292 | Nde.DirN(),Nde.File()) <= 0) | |
05eb7df0 | 293 | return _error->Error(_("The path is too long")); |
b2e465d6 AL |
294 | |
295 | // Deal with diversions | |
296 | if ((Nde->Flags & pkgFLCache::Node::Diversion) != 0) | |
297 | { | |
298 | pkgFLCache::DiverIterator Div = Nde.Diversion(); | |
299 | ||
300 | // See if it is us and we are following it in the right direction | |
301 | if (Div->OwnerPkg != FLPkg.Offset() && Div.DivertFrom() == Nde) | |
302 | { | |
303 | Nde = Div.DivertTo(); | |
304 | if (snprintf(FileName,sizeof(FileName)-20,"%s/%s", | |
305 | Nde.DirN(),Nde.File()) <= 0) | |
05eb7df0 | 306 | return _error->Error(_("The diversion path is too long")); |
b2e465d6 AL |
307 | } |
308 | } | |
309 | ||
310 | // Deal with overwrites+replaces | |
311 | for (; Nde.end() == false && Files->File == Nde->File; Nde++) | |
312 | { | |
313 | if ((Nde->Flags & pkgFLCache::Node::Replaced) == | |
314 | pkgFLCache::Node::Replaced) | |
315 | { | |
316 | if (Debug == true) | |
317 | clog << "De-replaced " << FileName << " from " << Nde.RealPackage()->Name << endl; | |
318 | Nde->Flags &= ~pkgFLCache::Node::Replaced; | |
319 | } | |
320 | } | |
321 | ||
322 | // Undo the change in the filesystem | |
323 | if (Debug == true) | |
324 | clog << "Backing out " << FileName; | |
325 | ||
326 | // Remove a new node | |
327 | if ((Files->Flags & pkgFLCache::Node::NewFile) == | |
328 | pkgFLCache::Node::NewFile) | |
329 | { | |
330 | if (Debug == true) | |
331 | clog << " [new node]" << endl; | |
332 | pkgFLCache::Node *Tmp = Files; | |
333 | Files++; | |
334 | *Last = Tmp->NextPkg; | |
335 | Tmp->NextPkg = 0; | |
336 | ||
337 | FLCache.DropNode(Tmp - FLCache.NodeP); | |
338 | } | |
339 | else | |
340 | { | |
341 | if (Debug == true) | |
342 | clog << endl; | |
343 | ||
344 | Last = &Files->NextPkg; | |
345 | Files++; | |
346 | } | |
347 | } | |
348 | ||
349 | return true; | |
350 | } | |
351 | /*}}}*/ | |
352 | // Extract::Fail - Extraction of a file Failed /*{{{*/ | |
353 | // --------------------------------------------------------------------- | |
354 | /* */ | |
355 | bool pkgExtract::Fail(Item &Itm,int Fd) | |
356 | { | |
357 | return pkgDirStream::Fail(Itm,Fd); | |
358 | } | |
359 | /*}}}*/ | |
360 | // Extract::FinishedFile - Finished a file /*{{{*/ | |
361 | // --------------------------------------------------------------------- | |
362 | /* */ | |
363 | bool pkgExtract::FinishedFile(Item &Itm,int Fd) | |
364 | { | |
365 | return pkgDirStream::FinishedFile(Itm,Fd); | |
366 | } | |
367 | /*}}}*/ | |
368 | // Extract::HandleOverwrites - See if a replaces covers this overwrite /*{{{*/ | |
369 | // --------------------------------------------------------------------- | |
370 | /* Check if the file is in a package that is being replaced by this | |
371 | package or if the file is being overwritten. Note that if the file | |
372 | is really a directory but it has been erased from the filesystem | |
373 | this will fail with an overwrite message. This is a limitation of the | |
374 | dpkg file information format. | |
375 | ||
376 | XX If a new package installs and another package replaces files in this | |
377 | package what should we do? */ | |
378 | bool pkgExtract::HandleOverwrites(pkgFLCache::NodeIterator Nde, | |
379 | bool DiverCheck) | |
380 | { | |
381 | pkgFLCache::NodeIterator TmpNde = Nde; | |
382 | unsigned long DiverOwner = 0; | |
383 | unsigned long FileGroup = Nde->File; | |
b2e465d6 AL |
384 | for (; Nde.end() == false && FileGroup == Nde->File; Nde++) |
385 | { | |
386 | if ((Nde->Flags & pkgFLCache::Node::Diversion) != 0) | |
387 | { | |
388 | /* Store the diversion owner if this is the forward direction | |
389 | of the diversion */ | |
390 | if (DiverCheck == true) | |
391 | DiverOwner = Nde.Diversion()->OwnerPkg; | |
392 | continue; | |
393 | } | |
394 | ||
395 | pkgFLCache::PkgIterator FPkg(FLCache,Nde.RealPackage()); | |
396 | if (FPkg.end() == true || FPkg == FLPkg) | |
397 | continue; | |
398 | ||
399 | /* This tests trips when we are checking a diversion to see | |
400 | if something has already been diverted by this diversion */ | |
401 | if (FPkg.Offset() == DiverOwner) | |
402 | continue; | |
e3d26885 | 403 | |
b2e465d6 AL |
404 | // Now see if this package matches one in a replace depends |
405 | pkgCache::DepIterator Dep = Ver.DependsList(); | |
406 | bool Ok = false; | |
804de19f | 407 | for (; Dep.end() == false; ++Dep) |
b2e465d6 AL |
408 | { |
409 | if (Dep->Type != pkgCache::Dep::Replaces) | |
410 | continue; | |
411 | ||
412 | // Does the replaces apply to this package? | |
413 | if (strcmp(Dep.TargetPkg().Name(),FPkg.Name()) != 0) | |
414 | continue; | |
415 | ||
416 | /* Check the version for match. I do not think CurrentVer can be | |
417 | 0 if we are here.. */ | |
418 | pkgCache::PkgIterator Pkg = Dep.TargetPkg(); | |
419 | if (Pkg->CurrentVer == 0) | |
420 | { | |
05eb7df0 | 421 | _error->Warning(_("Overwrite package match with no version for %s"),Pkg.Name()); |
b2e465d6 AL |
422 | continue; |
423 | } | |
424 | ||
425 | // Replaces is met | |
426 | if (debVS.CheckDep(Pkg.CurrentVer().VerStr(),Dep->CompareOp,Dep.TargetVer()) == true) | |
427 | { | |
428 | if (Debug == true) | |
429 | clog << "Replaced file " << Nde.DirN() << '/' << Nde.File() << " from " << Pkg.Name() << endl; | |
430 | Nde->Flags |= pkgFLCache::Node::Replaced; | |
431 | Ok = true; | |
432 | break; | |
433 | } | |
434 | } | |
435 | ||
436 | // Negative Hit | |
437 | if (Ok == false) | |
05eb7df0 | 438 | return _error->Error(_("File %s/%s overwrites the one in the package %s"), |
b2e465d6 AL |
439 | Nde.DirN(),Nde.File(),FPkg.Name()); |
440 | } | |
441 | ||
442 | /* If this is a diversion we might have to recurse to process | |
443 | the other side of it */ | |
444 | if ((TmpNde->Flags & pkgFLCache::Node::Diversion) != 0) | |
445 | { | |
446 | pkgFLCache::DiverIterator Div = TmpNde.Diversion(); | |
447 | if (Div.DivertTo() == TmpNde) | |
448 | return HandleOverwrites(Div.DivertFrom(),true); | |
449 | } | |
450 | ||
451 | return true; | |
452 | } | |
453 | /*}}}*/ | |
454 | // Extract::CheckDirReplace - See if this directory can be erased /*{{{*/ | |
455 | // --------------------------------------------------------------------- | |
456 | /* If this directory is owned by a single package and that package is | |
457 | replacing it with something non-directoryish then dpkg allows this. | |
458 | We increase the requirement to be that the directory is non-empty after | |
459 | the package is removed */ | |
460 | bool pkgExtract::CheckDirReplace(string Dir,unsigned int Depth) | |
461 | { | |
462 | // Looping? | |
463 | if (Depth > 40) | |
464 | return false; | |
465 | ||
466 | if (Dir[Dir.size() - 1] != '/') | |
467 | Dir += '/'; | |
468 | ||
469 | DIR *D = opendir(Dir.c_str()); | |
470 | if (D == 0) | |
05eb7df0 | 471 | return _error->Errno("opendir",_("Unable to read %s"),Dir.c_str()); |
b2e465d6 AL |
472 | |
473 | string File; | |
474 | for (struct dirent *Dent = readdir(D); Dent != 0; Dent = readdir(D)) | |
475 | { | |
476 | // Skip some files | |
477 | if (strcmp(Dent->d_name,".") == 0 || | |
478 | strcmp(Dent->d_name,"..") == 0) | |
479 | continue; | |
480 | ||
481 | // Look up the node | |
482 | File = Dir + Dent->d_name; | |
18255546 AL |
483 | pkgFLCache::NodeIterator Nde = FLCache.GetNode(File.c_str(), |
484 | File.c_str() + File.length(),0,false,false); | |
b2e465d6 AL |
485 | |
486 | // The file is not owned by this package | |
487 | if (Nde.end() != false || Nde.RealPackage() != FLPkg) | |
488 | { | |
489 | closedir(D); | |
490 | return false; | |
491 | } | |
492 | ||
493 | // See if it is a directory | |
494 | struct stat St; | |
495 | if (lstat(File.c_str(),&St) != 0) | |
496 | { | |
497 | closedir(D); | |
05eb7df0 | 498 | return _error->Errno("lstat",_("Unable to stat %s"),File.c_str()); |
b2e465d6 AL |
499 | } |
500 | ||
501 | // Recurse down directories | |
502 | if (S_ISDIR(St.st_mode) != 0) | |
503 | { | |
504 | if (CheckDirReplace(File,Depth + 1) == false) | |
505 | { | |
506 | closedir(D); | |
507 | return false; | |
508 | } | |
509 | } | |
510 | } | |
511 | ||
512 | // No conflicts | |
513 | closedir(D); | |
514 | return true; | |
515 | } | |
516 | /*}}}*/ |