]> git.saurik.com Git - apt.git/blob - apt-pkg/cdrom.cc
Merge with Matt and update French translation
[apt.git] / apt-pkg / cdrom.cc
1 /*
2 */
3
4 #ifdef __GNUG__
5 #pragma implementation "apt-pkg/cdrom.h"
6 #endif
7 #include<apt-pkg/init.h>
8 #include<apt-pkg/error.h>
9 #include<apt-pkg/cdromutl.h>
10 #include<apt-pkg/strutl.h>
11 #include<apt-pkg/cdrom.h>
12 #include<sstream>
13 #include<fstream>
14 #include<config.h>
15 #include<apti18n.h>
16 #include <sys/stat.h>
17 #include <fcntl.h>
18 #include <dirent.h>
19 #include <unistd.h>
20 #include <stdio.h>
21
22
23 #include "indexcopy.h"
24
25 using namespace std;
26
27 // FindPackages - Find the package files on the CDROM /*{{{*/
28 // ---------------------------------------------------------------------
29 /* We look over the cdrom for package files. This is a recursive
30 search that short circuits when it his a package file in the dir.
31 This speeds it up greatly as the majority of the size is in the
32 binary-* sub dirs. */
33 bool pkgCdrom::FindPackages(string CD,vector<string> &List,
34 vector<string> &SList, vector<string> &SigList,
35 string &InfoDir, pkgCdromStatus *log,
36 unsigned int Depth)
37 {
38 static ino_t Inodes[9];
39
40 // if we have a look we "pulse" now
41 if(log)
42 log->Update();
43
44 if (Depth >= 7)
45 return true;
46
47 if (CD[CD.length()-1] != '/')
48 CD += '/';
49
50 if (chdir(CD.c_str()) != 0)
51 return _error->Errno("chdir","Unable to change to %s",CD.c_str());
52
53 // Look for a .disk subdirectory
54 struct stat Buf;
55 if (stat(".disk",&Buf) == 0)
56 {
57 if (InfoDir.empty() == true)
58 InfoDir = CD + ".disk/";
59 }
60
61 // Don't look into directories that have been marked to ingore.
62 if (stat(".aptignr",&Buf) == 0)
63 return true;
64
65
66 /* Check _first_ for a signature file as apt-cdrom assumes that all files
67 under a Packages/Source file are in control of that file and stops
68 the scanning
69 */
70 if (stat("Release.gpg",&Buf) == 0)
71 {
72 SigList.push_back(CD);
73 }
74 /* Aha! We found some package files. We assume that everything under
75 this dir is controlled by those package files so we don't look down
76 anymore */
77 if (stat("Packages",&Buf) == 0 || stat("Packages.gz",&Buf) == 0)
78 {
79 List.push_back(CD);
80
81 // Continue down if thorough is given
82 if (_config->FindB("APT::CDROM::Thorough",false) == false)
83 return true;
84 }
85 if (stat("Sources.gz",&Buf) == 0 || stat("Sources",&Buf) == 0)
86 {
87 SList.push_back(CD);
88
89 // Continue down if thorough is given
90 if (_config->FindB("APT::CDROM::Thorough",false) == false)
91 return true;
92 }
93
94 DIR *D = opendir(".");
95 if (D == 0)
96 return _error->Errno("opendir","Unable to read %s",CD.c_str());
97
98 // Run over the directory
99 for (struct dirent *Dir = readdir(D); Dir != 0; Dir = readdir(D))
100 {
101 // Skip some files..
102 if (strcmp(Dir->d_name,".") == 0 ||
103 strcmp(Dir->d_name,"..") == 0 ||
104 //strcmp(Dir->d_name,"source") == 0 ||
105 strcmp(Dir->d_name,".disk") == 0 ||
106 strcmp(Dir->d_name,"experimental") == 0 ||
107 strcmp(Dir->d_name,"binary-all") == 0 ||
108 strcmp(Dir->d_name,"debian-installer") == 0)
109 continue;
110
111 // See if the name is a sub directory
112 struct stat Buf;
113 if (stat(Dir->d_name,&Buf) != 0)
114 continue;
115
116 if (S_ISDIR(Buf.st_mode) == 0)
117 continue;
118
119 unsigned int I;
120 for (I = 0; I != Depth; I++)
121 if (Inodes[I] == Buf.st_ino)
122 break;
123 if (I != Depth)
124 continue;
125
126 // Store the inodes weve seen
127 Inodes[Depth] = Buf.st_ino;
128
129 // Descend
130 if (FindPackages(CD + Dir->d_name,List,SList,SigList,InfoDir,log,Depth+1) == false)
131 break;
132
133 if (chdir(CD.c_str()) != 0)
134 return _error->Errno("chdir","Unable to change to %s",CD.c_str());
135 };
136
137 closedir(D);
138
139 return !_error->PendingError();
140 }
141
142 // Score - We compute a 'score' for a path /*{{{*/
143 // ---------------------------------------------------------------------
144 /* Paths are scored based on how close they come to what I consider
145 normal. That is ones that have 'dist' 'stable' 'testing' will score
146 higher than ones without. */
147 int pkgCdrom::Score(string Path)
148 {
149 int Res = 0;
150 if (Path.find("stable/") != string::npos)
151 Res += 29;
152 if (Path.find("/binary-") != string::npos)
153 Res += 20;
154 if (Path.find("testing/") != string::npos)
155 Res += 28;
156 if (Path.find("unstable/") != string::npos)
157 Res += 27;
158 if (Path.find("/dists/") != string::npos)
159 Res += 40;
160 if (Path.find("/main/") != string::npos)
161 Res += 20;
162 if (Path.find("/contrib/") != string::npos)
163 Res += 20;
164 if (Path.find("/non-free/") != string::npos)
165 Res += 20;
166 if (Path.find("/non-US/") != string::npos)
167 Res += 20;
168 if (Path.find("/source/") != string::npos)
169 Res += 10;
170 if (Path.find("/debian/") != string::npos)
171 Res -= 10;
172 return Res;
173 }
174
175 /*}}}*/
176 // DropBinaryArch - Dump dirs with a string like /binary-<foo>/ /*{{{*/
177 // ---------------------------------------------------------------------
178 /* Here we drop everything that is not this machines arch */
179 bool pkgCdrom::DropBinaryArch(vector<string> &List)
180 {
181 char S[300];
182 snprintf(S,sizeof(S),"/binary-%s/",
183 _config->Find("Apt::Architecture").c_str());
184
185 for (unsigned int I = 0; I < List.size(); I++)
186 {
187 const char *Str = List[I].c_str();
188
189 const char *Res;
190 if ((Res = strstr(Str,"/binary-")) == 0)
191 continue;
192
193 // Weird, remove it.
194 if (strlen(Res) < strlen(S))
195 {
196 List.erase(List.begin() + I);
197 I--;
198 continue;
199 }
200
201 // See if it is our arch
202 if (stringcmp(Res,Res + strlen(S),S) == 0)
203 continue;
204
205 // Erase it
206 List.erase(List.begin() + I);
207 I--;
208 }
209
210 return true;
211 }
212
213
214 // DropRepeats - Drop repeated files resulting from symlinks /*{{{*/
215 // ---------------------------------------------------------------------
216 /* Here we go and stat every file that we found and strip dup inodes. */
217 bool pkgCdrom::DropRepeats(vector<string> &List,const char *Name)
218 {
219 // Get a list of all the inodes
220 ino_t *Inodes = new ino_t[List.size()];
221 for (unsigned int I = 0; I != List.size(); I++)
222 {
223 struct stat Buf;
224 if (stat((List[I] + Name).c_str(),&Buf) != 0 &&
225 stat((List[I] + Name + ".gz").c_str(),&Buf) != 0)
226 _error->Errno("stat","Failed to stat %s%s",List[I].c_str(),
227 Name);
228 Inodes[I] = Buf.st_ino;
229 }
230
231 if (_error->PendingError() == true)
232 return false;
233
234 // Look for dups
235 for (unsigned int I = 0; I != List.size(); I++)
236 {
237 for (unsigned int J = I+1; J < List.size(); J++)
238 {
239 // No match
240 if (Inodes[J] != Inodes[I])
241 continue;
242
243 // We score the two paths.. and erase one
244 int ScoreA = Score(List[I]);
245 int ScoreB = Score(List[J]);
246 if (ScoreA < ScoreB)
247 {
248 List[I] = string();
249 break;
250 }
251
252 List[J] = string();
253 }
254 }
255
256 // Wipe erased entries
257 for (unsigned int I = 0; I < List.size();)
258 {
259 if (List[I].empty() == false)
260 I++;
261 else
262 List.erase(List.begin()+I);
263 }
264
265 return true;
266 }
267 /*}}}*/
268
269 // ReduceSourceList - Takes the path list and reduces it /*{{{*/
270 // ---------------------------------------------------------------------
271 /* This takes the list of source list expressed entires and collects
272 similar ones to form a single entry for each dist */
273 void pkgCdrom::ReduceSourcelist(string CD,vector<string> &List)
274 {
275 sort(List.begin(),List.end());
276
277 // Collect similar entries
278 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
279 {
280 // Find a space..
281 string::size_type Space = (*I).find(' ');
282 if (Space == string::npos)
283 continue;
284 string::size_type SSpace = (*I).find(' ',Space + 1);
285 if (SSpace == string::npos)
286 continue;
287
288 string Word1 = string(*I,Space,SSpace-Space);
289 string Prefix = string(*I,0,Space);
290 for (vector<string>::iterator J = List.begin(); J != I; J++)
291 {
292 // Find a space..
293 string::size_type Space2 = (*J).find(' ');
294 if (Space2 == string::npos)
295 continue;
296 string::size_type SSpace2 = (*J).find(' ',Space2 + 1);
297 if (SSpace2 == string::npos)
298 continue;
299
300 if (string(*J,0,Space2) != Prefix)
301 continue;
302 if (string(*J,Space2,SSpace2-Space2) != Word1)
303 continue;
304
305 *J += string(*I,SSpace);
306 *I = string();
307 }
308 }
309
310 // Wipe erased entries
311 for (unsigned int I = 0; I < List.size();)
312 {
313 if (List[I].empty() == false)
314 I++;
315 else
316 List.erase(List.begin()+I);
317 }
318 }
319 /*}}}*/
320 // WriteDatabase - Write the CDROM Database file /*{{{*/
321 // ---------------------------------------------------------------------
322 /* We rewrite the configuration class associated with the cdrom database. */
323 bool pkgCdrom::WriteDatabase(Configuration &Cnf)
324 {
325 string DFile = _config->FindFile("Dir::State::cdroms");
326 string NewFile = DFile + ".new";
327
328 unlink(NewFile.c_str());
329 ofstream Out(NewFile.c_str());
330 if (!Out)
331 return _error->Errno("ofstream::ofstream",
332 "Failed to open %s.new",DFile.c_str());
333
334 /* Write out all of the configuration directives by walking the
335 configuration tree */
336 const Configuration::Item *Top = Cnf.Tree(0);
337 for (; Top != 0;)
338 {
339 // Print the config entry
340 if (Top->Value.empty() == false)
341 Out << Top->FullTag() + " \"" << Top->Value << "\";" << endl;
342
343 if (Top->Child != 0)
344 {
345 Top = Top->Child;
346 continue;
347 }
348
349 while (Top != 0 && Top->Next == 0)
350 Top = Top->Parent;
351 if (Top != 0)
352 Top = Top->Next;
353 }
354
355 Out.close();
356
357 rename(DFile.c_str(),string(DFile + '~').c_str());
358 if (rename(NewFile.c_str(),DFile.c_str()) != 0)
359 return _error->Errno("rename","Failed to rename %s.new to %s",
360 DFile.c_str(),DFile.c_str());
361
362 return true;
363 }
364 /*}}}*/
365 // WriteSourceList - Write an updated sourcelist /*{{{*/
366 // ---------------------------------------------------------------------
367 /* This reads the old source list and copies it into the new one. It
368 appends the new CDROM entires just after the first block of comments.
369 This places them first in the file. It also removes any old entries
370 that were the same. */
371 bool pkgCdrom::WriteSourceList(string Name,vector<string> &List,bool Source)
372 {
373 if (List.size() == 0)
374 return true;
375
376 string File = _config->FindFile("Dir::Etc::sourcelist");
377
378 // Open the stream for reading
379 ifstream F((FileExists(File)?File.c_str():"/dev/null"),
380 ios::in );
381 if (!F != 0)
382 return _error->Errno("ifstream::ifstream","Opening %s",File.c_str());
383
384 string NewFile = File + ".new";
385 unlink(NewFile.c_str());
386 ofstream Out(NewFile.c_str());
387 if (!Out)
388 return _error->Errno("ofstream::ofstream",
389 "Failed to open %s.new",File.c_str());
390
391 // Create a short uri without the path
392 string ShortURI = "cdrom:[" + Name + "]/";
393 string ShortURI2 = "cdrom:" + Name + "/"; // For Compatibility
394
395 string Type;
396 if (Source == true)
397 Type = "deb-src";
398 else
399 Type = "deb";
400
401 char Buffer[300];
402 int CurLine = 0;
403 bool First = true;
404 while (F.eof() == false)
405 {
406 F.getline(Buffer,sizeof(Buffer));
407 CurLine++;
408 _strtabexpand(Buffer,sizeof(Buffer));
409 _strstrip(Buffer);
410
411 // Comment or blank
412 if (Buffer[0] == '#' || Buffer[0] == 0)
413 {
414 Out << Buffer << endl;
415 continue;
416 }
417
418 if (First == true)
419 {
420 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
421 {
422 string::size_type Space = (*I).find(' ');
423 if (Space == string::npos)
424 return _error->Error("Internal error");
425 Out << Type << " cdrom:[" << Name << "]/" << string(*I,0,Space) <<
426 " " << string(*I,Space+1) << endl;
427 }
428 }
429 First = false;
430
431 // Grok it
432 string cType;
433 string URI;
434 const char *C = Buffer;
435 if (ParseQuoteWord(C,cType) == false ||
436 ParseQuoteWord(C,URI) == false)
437 {
438 Out << Buffer << endl;
439 continue;
440 }
441
442 // Emit lines like this one
443 if (cType != Type || (string(URI,0,ShortURI.length()) != ShortURI &&
444 string(URI,0,ShortURI.length()) != ShortURI2))
445 {
446 Out << Buffer << endl;
447 continue;
448 }
449 }
450
451 // Just in case the file was empty
452 if (First == true)
453 {
454 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
455 {
456 string::size_type Space = (*I).find(' ');
457 if (Space == string::npos)
458 return _error->Error("Internal error");
459
460 Out << "deb cdrom:[" << Name << "]/" << string(*I,0,Space) <<
461 " " << string(*I,Space+1) << endl;
462 }
463 }
464
465 Out.close();
466
467 rename(File.c_str(),string(File + '~').c_str());
468 if (rename(NewFile.c_str(),File.c_str()) != 0)
469 return _error->Errno("rename","Failed to rename %s.new to %s",
470 File.c_str(),File.c_str());
471
472 return true;
473 }
474
475
476 bool pkgCdrom::Ident(string &ident, pkgCdromStatus *log)
477 {
478 stringstream msg;
479
480 // Startup
481 string CDROM = _config->FindDir("Acquire::cdrom::mount","/cdrom/");
482 if (CDROM[0] == '.')
483 CDROM= SafeGetCWD() + '/' + CDROM;
484
485 if(log) {
486 msg.str("");
487 ioprintf(msg, _("Using CD-ROM mount point %s\nMounting CD-ROM\n"),
488 CDROM.c_str());
489 log->Update(msg.str());
490 }
491 if (MountCdrom(CDROM) == false)
492 return _error->Error("Failed to mount the cdrom.");
493
494 // Hash the CD to get an ID
495 if(log)
496 log->Update(_("Identifying.. "));
497
498
499 if (IdentCdrom(CDROM,ident) == false)
500 {
501 ident = "";
502 return false;
503 }
504
505 msg.str("");
506 ioprintf(msg, "[%s]\n",ident.c_str());
507 log->Update(msg.str());
508
509
510 // Read the database
511 Configuration Database;
512 string DFile = _config->FindFile("Dir::State::cdroms");
513 if (FileExists(DFile) == true)
514 {
515 if (ReadConfigFile(Database,DFile) == false)
516 return _error->Error("Unable to read the cdrom database %s",
517 DFile.c_str());
518 }
519 if(log) {
520 msg.str("");
521 ioprintf(msg, _("Stored Label: %s \n"),
522 Database.Find("CD::"+ident).c_str());
523 log->Update(msg.str());
524 }
525 return true;
526 }
527
528
529 bool pkgCdrom::Add(pkgCdromStatus *log)
530 {
531 stringstream msg;
532
533 // Startup
534 string CDROM = _config->FindDir("Acquire::cdrom::mount","/cdrom/");
535 if (CDROM[0] == '.')
536 CDROM= SafeGetCWD() + '/' + CDROM;
537
538 if(log) {
539 log->SetTotal(STEP_LAST);
540 msg.str("");
541 ioprintf(msg, _("Using CD-ROM mount point %s\n"), CDROM.c_str());
542 log->Update(msg.str(), STEP_PREPARE);
543 }
544
545 // Read the database
546 Configuration Database;
547 string DFile = _config->FindFile("Dir::State::cdroms");
548 if (FileExists(DFile) == true)
549 {
550 if (ReadConfigFile(Database,DFile) == false)
551 return _error->Error("Unable to read the cdrom database %s",
552 DFile.c_str());
553 }
554
555 // Unmount the CD and get the user to put in the one they want
556 if (_config->FindB("APT::CDROM::NoMount",false) == false)
557 {
558 if(log)
559 log->Update(_("Unmounting CD-ROM\n"), STEP_UNMOUNT);
560 UnmountCdrom(CDROM);
561
562 if(log) {
563 log->Update(_("Waiting for disc...\n"), STEP_WAIT);
564 if(!log->ChangeCdrom()) {
565 // user aborted
566 return false;
567 }
568 }
569
570 // Mount the new CDROM
571 log->Update(_("Mounting CD-ROM...\n"), STEP_MOUNT);
572 if (MountCdrom(CDROM) == false)
573 return _error->Error("Failed to mount the cdrom.");
574 }
575
576 // Hash the CD to get an ID
577 if(log)
578 log->Update(_("Identifying.. "), STEP_IDENT);
579 string ID;
580 if (IdentCdrom(CDROM,ID) == false)
581 {
582 log->Update("\n");
583 return false;
584 }
585 if(log)
586 log->Update("["+ID+"]\n");
587
588 if(log)
589 log->Update(_("Scanning Disc for index files..\n"),STEP_SCAN);
590
591 // Get the CD structure
592 vector<string> List;
593 vector<string> SourceList;
594 vector<string> SigList;
595 string StartDir = SafeGetCWD();
596 string InfoDir;
597 if (FindPackages(CDROM,List,SourceList, SigList,InfoDir,log) == false)
598 {
599 log->Update("\n");
600 return false;
601 }
602
603 chdir(StartDir.c_str());
604
605 if (_config->FindB("Debug::aptcdrom",false) == true)
606 {
607 cout << "I found (binary):" << endl;
608 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
609 cout << *I << endl;
610 cout << "I found (source):" << endl;
611 for (vector<string>::iterator I = SourceList.begin(); I != SourceList.end(); I++)
612 cout << *I << endl;
613 cout << "I found (Signatures):" << endl;
614 for (vector<string>::iterator I = SigList.begin(); I != SigList.end(); I++)
615 cout << *I << endl;
616 }
617
618 //log->Update(_("Cleaning package lists..."), STEP_CLEAN);
619
620 // Fix up the list
621 DropBinaryArch(List);
622 DropRepeats(List,"Packages");
623 DropRepeats(SourceList,"Sources");
624 DropRepeats(SigList,"Release.gpg");
625 if(log) {
626 msg.str("");
627 ioprintf(msg, _("Found %i package indexes, %i source indexes and "
628 "%i signatures\n"),
629 List.size(), SourceList.size(), SigList.size());
630 log->Update(msg.str(), STEP_SCAN);
631 }
632
633 if (List.size() == 0 && SourceList.size() == 0)
634 return _error->Error("Unable to locate any package files, perhaps this is not a Debian Disc");
635
636 // Check if the CD is in the database
637 string Name;
638 if (Database.Exists("CD::" + ID) == false ||
639 _config->FindB("APT::CDROM::Rename",false) == true)
640 {
641 // Try to use the CDs label if at all possible
642 if (InfoDir.empty() == false &&
643 FileExists(InfoDir + "/info") == true)
644 {
645 ifstream F(string(InfoDir + "/info").c_str());
646 if (!F == 0)
647 getline(F,Name);
648
649 if (Name.empty() == false)
650 {
651 // Escape special characters
652 string::iterator J = Name.begin();
653 for (; J != Name.end(); J++)
654 if (*J == '"' || *J == ']' || *J == '[')
655 *J = '_';
656
657 if(log) {
658 msg.str("");
659 ioprintf(msg, "Found label '%s'\n", Name.c_str());
660 log->Update(msg.str());
661 }
662 Database.Set("CD::" + ID + "::Label",Name);
663 }
664 }
665
666 if (_config->FindB("APT::CDROM::Rename",false) == true ||
667 Name.empty() == true)
668 {
669 if(!log)
670 return _error->Error("No disc name found and no way to ask for it");
671
672 while(true) {
673 if(!log->AskCdromName(Name)) {
674 // user canceld
675 return false;
676 }
677 cout << "Name: '" << Name << "'" << endl;
678
679 if (Name.empty() == false &&
680 Name.find('"') == string::npos &&
681 Name.find('[') == string::npos &&
682 Name.find(']') == string::npos)
683 break;
684 log->Update(_("That is not a valid name, try again.\n"));
685 }
686 }
687 }
688 else
689 Name = Database.Find("CD::" + ID);
690
691 // Escape special characters
692 string::iterator J = Name.begin();
693 for (; J != Name.end(); J++)
694 if (*J == '"' || *J == ']' || *J == '[')
695 *J = '_';
696
697 Database.Set("CD::" + ID,Name);
698 if(log) {
699 msg.str("");
700 ioprintf(msg, _("This Disc is called: \n'%s'\n"), Name.c_str());
701 log->Update(msg.str());
702 }
703
704 log->Update(_("Copying package lists..."), STEP_COPY);
705 // take care of the signatures and copy them if they are ok
706 // (we do this before PackageCopy as it modifies "List" and "SourceList")
707 SigVerify SignVerify;
708 SignVerify.CopyAndVerify(CDROM, Name, SigList, List, SourceList);
709
710 // Copy the package files to the state directory
711 PackageCopy Copy;
712 SourceCopy SrcCopy;
713 if (Copy.CopyPackages(CDROM,Name,List, log) == false ||
714 SrcCopy.CopyPackages(CDROM,Name,SourceList, log) == false)
715 return false;
716
717 // reduce the List so that it takes less space in sources.list
718 ReduceSourcelist(CDROM,List);
719 ReduceSourcelist(CDROM,SourceList);
720
721 // Write the database and sourcelist
722 if (_config->FindB("APT::cdrom::NoAct",false) == false)
723 {
724 if (WriteDatabase(Database) == false)
725 return false;
726
727 if(log) {
728 log->Update(_("Writing new source list\n"), STEP_WRITE);
729 }
730 if (WriteSourceList(Name,List,false) == false ||
731 WriteSourceList(Name,SourceList,true) == false)
732 return false;
733 }
734
735 // Print the sourcelist entries
736 if(log)
737 log->Update(_("Source List entries for this Disc are:\n"));
738
739 for (vector<string>::iterator I = List.begin(); I != List.end(); I++)
740 {
741 string::size_type Space = (*I).find(' ');
742 if (Space == string::npos)
743 return _error->Error("Internal error");
744
745 if(log) {
746 msg.str("");
747 msg << "deb cdrom:[" << Name << "]/" << string(*I,0,Space) <<
748 " " << string(*I,Space+1) << endl;
749 log->Update(msg.str());
750 }
751 }
752
753 for (vector<string>::iterator I = SourceList.begin(); I != SourceList.end(); I++)
754 {
755 string::size_type Space = (*I).find(' ');
756 if (Space == string::npos)
757 return _error->Error("Internal error");
758
759 if(log) {
760 msg.str("");
761 msg << "deb-src cdrom:[" << Name << "]/" << string(*I,0,Space) <<
762 " " << string(*I,Space+1) << endl;
763 log->Update(msg.str());
764 }
765 }
766
767
768
769 // Unmount and finish
770 if (_config->FindB("APT::CDROM::NoMount",false) == false) {
771 log->Update(_("Unmounting CD-ROM..."), STEP_LAST);
772 UnmountCdrom(CDROM);
773 }
774
775 return true;
776 }