1 /*---------------------------------------------------------------------\
3 | |__ / \ / / . \ . \ |
8 \---------------------------------------------------------------------*/
9 /** \file zypp/Fetcher.cc
17 #include "zypp/base/Easy.h"
18 #include "zypp/base/Logger.h"
19 #include "zypp/base/PtrTypes.h"
20 #include "zypp/base/DefaultIntegral.h"
21 #include "zypp/base/String.h"
22 #include "zypp/Fetcher.h"
23 #include "zypp/CheckSum.h"
24 #include "zypp/base/UserRequestException.h"
25 #include "zypp/parser/susetags/ContentFileReader.h"
26 #include "zypp/parser/susetags/RepoIndex.h"
30 ///////////////////////////////////////////////////////////////////
32 { /////////////////////////////////////////////////////////////////
35 * class that represents indexes which add metadata
36 * to fetcher jobs and therefore need to be retrieved
41 FetcherIndex( const OnMediaLocation &loc )
46 OnMediaLocation location;
48 typedef shared_ptr<FetcherIndex> FetcherIndex_Ptr;
51 * Class to encapsulate the \ref OnMediaLocation object
52 * and the \ref FileChecker together
61 RecursiveDirectory = Directory | Recursive,
62 // check checksums even if there is no such
63 // checksum (warns of no checksum)
64 AlwaysVerifyChecksum = 0x0004,
66 ZYPP_DECLARE_FLAGS(Flags, Flag);
69 FetcherJob( const OnMediaLocation &loc )
73 //MIL << location << endl;
78 //MIL << location << " | * " << checkers.size() << endl;
81 OnMediaLocation location;
82 //CompositeFileChecker checkers;
83 list<FileChecker> checkers;
86 ZYPP_DECLARE_OPERATORS_FOR_FLAGS(FetcherJob::Flags);
87 typedef shared_ptr<FetcherJob> FetcherJob_Ptr;
89 std::ostream & operator<<( std::ostream & str, const FetcherJob_Ptr & obj )
91 return str << obj->location;
95 ///////////////////////////////////////////////////////////////////
97 // CLASS NAME : Fetcher::Impl
99 /** Fetcher implementation. */
102 friend std::ostream & operator<<( std::ostream & str, const Fetcher::Impl & obj );
110 void setOptions( Fetcher::Options options );
111 Fetcher::Options options() const;
113 void addIndex( const OnMediaLocation &resource );
115 void enqueueDir( const OnMediaLocation &resource, bool recursive, const FileChecker &checker = FileChecker() );
116 void enqueueDigestedDir( const OnMediaLocation &resource, bool recursive, const FileChecker &checker = FileChecker() );
118 void enqueue( const OnMediaLocation &resource, const FileChecker &checker = FileChecker() );
119 void enqueueDigested( const OnMediaLocation &resource, const FileChecker &checker = FileChecker() );
120 void addCachePath( const Pathname &cache_dir );
122 void start( const Pathname &dest_dir,
123 MediaSetAccess &media,
124 const ProgressData::ReceiverFnc & progress_receiver );
126 /** Offer default Impl. */
127 static shared_ptr<Impl> nullimpl()
129 static shared_ptr<Impl> _nullimpl( new Impl );
134 * download the indexes and reads them
136 void downloadAndReadIndexList( MediaSetAccess &media, const Pathname &dest_dir);
139 * download the indexes and reads them
141 void downloadIndex( MediaSetAccess &media, const OnMediaLocation &resource, const Pathname &dest_dir);
144 * reads a downloaded index file and updates internal
147 * The index lists files relative to a directory, which is
148 * normally the same as the index file is located.
150 void readIndex( const Pathname &index, const Pathname &basedir );
152 /** specific version of \ref readIndex for SHA1SUMS file */
153 void readSha1sumsIndex( const Pathname &index, const Pathname &basedir );
155 /** specific version of \ref readIndex for SHA1SUMS file */
156 void readContentFileIndex( const Pathname &index, const Pathname &basedir );
158 /** reads the content of a directory but keeps a cache **/
159 void getDirectoryContent( MediaSetAccess &media, const OnMediaLocation &resource, filesystem::DirContent &content );
162 * tries to provide the file represented by job into dest_dir by
163 * looking at the cache. If success, returns true, and the desired
164 * file should be available on dest_dir
166 bool provideFromCache( const OnMediaLocation &resource, const Pathname &dest_dir );
168 * Validates the job against is checkers, by using the file instance
172 void validate( const OnMediaLocation &resource, const Pathname &dest_dir, const list<FileChecker> &checkers );
175 * scan the directory and adds the individual jobs
177 void addDirJobs( MediaSetAccess &media, const OnMediaLocation &resource,
178 const Pathname &dest_dir, FetcherJob::Flags flags );
181 * auto discovery and reading of indexes
183 void autoaddIndexes( const filesystem::DirContent &content,
184 MediaSetAccess &media,
185 const OnMediaLocation &resource,
186 const Pathname &dest_dir );
188 * Provide the resource to \ref dest_dir
190 void provideToDest( MediaSetAccess &media, const OnMediaLocation &resource, const Pathname &dest_dir );
193 friend Impl * rwcowClone<Impl>( const Impl * rhs );
194 /** clone for RWCOW_pointer */
196 { return new Impl( *this ); }
198 list<FetcherJob_Ptr> _resources;
199 list<FetcherIndex_Ptr> _indexes;
200 list<Pathname> _caches;
201 // checksums read from the indexes
202 map<string, CheckSum> _checksums;
203 // cache of dir contents
204 map<string, filesystem::DirContent> _dircontent;
206 Fetcher::Options _options;
208 ///////////////////////////////////////////////////////////////////
210 void Fetcher::Impl::enqueueDigested( const OnMediaLocation &resource, const FileChecker &checker )
213 job.reset(new FetcherJob(resource));
214 job->flags |= FetcherJob:: AlwaysVerifyChecksum;
215 _resources.push_back(job);
218 Fetcher::Impl::Impl()
223 void Fetcher::Impl::setOptions( Fetcher::Options options )
224 { _options = options; }
226 Fetcher::Options Fetcher::Impl::options() const
229 void Fetcher::Impl::enqueueDir( const OnMediaLocation &resource,
231 const FileChecker &checker )
234 job.reset(new FetcherJob(resource));
236 job->checkers.push_back(checker);
238 job->flags |= FetcherJob::Recursive;
239 job->flags |= FetcherJob::Directory;
241 _resources.push_back(job);
244 void Fetcher::Impl::enqueueDigestedDir( const OnMediaLocation &resource,
246 const FileChecker &checker )
249 job.reset(new FetcherJob(resource));
251 job->checkers.push_back(checker);
253 job->flags |= FetcherJob::Recursive;
254 job->flags |= FetcherJob::Directory;
255 job->flags |= FetcherJob::AlwaysVerifyChecksum;
257 _resources.push_back(job);
261 void Fetcher::Impl::enqueue( const OnMediaLocation &resource, const FileChecker &checker )
264 job.reset(new FetcherJob(resource));
266 job->checkers.push_back(checker);
267 _resources.push_back(job);
270 void Fetcher::Impl::addIndex( const OnMediaLocation &resource )
272 MIL << "adding index " << resource << endl;
273 FetcherIndex_Ptr index;
274 index.reset(new FetcherIndex(resource));
275 _indexes.push_back(index);
279 void Fetcher::Impl::reset()
287 void Fetcher::Impl::addCachePath( const Pathname &cache_dir )
289 PathInfo info(cache_dir);
290 if ( info.isExist() )
294 DBG << "Adding fetcher cache: '" << cache_dir << "'." << endl;
295 _caches.push_back(cache_dir);
299 // don't add bad cache directory, just log the error
300 ERR << "Not adding cache: '" << cache_dir << "'. Not a directory." << endl;
305 ERR << "Not adding cache '" << cache_dir << "'. Path does not exists." << endl;
310 // tries to provide resource to dest_dir from any of the configured additional
311 // cache paths where the file may already be present. returns true if the
312 // file was provided from the cache.
313 bool Fetcher::Impl::provideFromCache( const OnMediaLocation &resource, const Pathname &dest_dir )
315 Pathname dest_full_path = dest_dir + resource.filename();
317 // first check in the destination directory
318 if ( PathInfo(dest_full_path).isExist() )
320 if ( is_checksum( dest_full_path, resource.checksum() )
321 && (! resource.checksum().empty() ) )
325 MIL << "start fetcher with " << _caches.size() << " cache directories." << endl;
326 for_ ( it_cache, _caches.begin(), _caches.end() )
328 // does the current file exists in the current cache?
329 Pathname cached_file = *it_cache + resource.filename();
330 if ( PathInfo( cached_file ).isExist() )
332 DBG << "File '" << cached_file << "' exist, testing checksum " << resource.checksum() << endl;
333 // check the checksum
334 if ( is_checksum( cached_file, resource.checksum() ) && (! resource.checksum().empty() ) )
337 MIL << "file " << resource.filename() << " found in previous cache. Using cached copy." << endl;
338 // checksum is already checked.
339 // we could later implement double failover and try to download if file copy fails.
340 // replicate the complete path in the target directory
341 if( dest_full_path != cached_file )
343 if ( assert_dir( dest_full_path.dirname() ) != 0 )
344 ZYPP_THROW( Exception("Can't create " + dest_full_path.dirname().asString()));
346 if ( filesystem::hardlink(cached_file, dest_full_path ) != 0 )
348 WAR << "Can't hardlink '" << cached_file << "' to '" << dest_dir << "'. Trying copying." << endl;
349 if ( filesystem::copy(cached_file, dest_full_path ) != 0 )
351 ERR << "Can't copy " << cached_file + " to " + dest_dir << endl;
361 } // iterate over caches
365 void Fetcher::Impl::validate( const OnMediaLocation &resource, const Pathname &dest_dir, const list<FileChecker> &checkers )
367 // no matter where did we got the file, try to validate it:
368 Pathname localfile = dest_dir + resource.filename();
369 // call the checker function
372 MIL << "Checking job [" << localfile << "] (" << checkers.size() << " checkers )" << endl;
374 for ( list<FileChecker>::const_iterator it = checkers.begin();
375 it != checkers.end();
384 ERR << "Invalid checker for '" << localfile << "'" << endl;
389 catch ( const FileCheckException &e )
393 catch ( const Exception &e )
399 ZYPP_THROW(Exception("Unknown error while validating " + resource.filename().asString()));
403 void Fetcher::Impl::autoaddIndexes( const filesystem::DirContent &content,
404 MediaSetAccess &media,
405 const OnMediaLocation &resource,
406 const Pathname &dest_dir )
408 if ( _options & AutoAddSha1sumsIndexes )
410 // only try to add an index if it exists
411 filesystem::DirEntry shafile;
412 shafile.name = "SHA1SUMS"; shafile.type = filesystem::FT_FILE;
413 if ( find( content.begin(), content.end(), shafile ) != content.end() )
415 // add the index of this directory
416 OnMediaLocation indexloc(resource);
417 indexloc.changeFilename(resource.filename() + "SHA1SUMS");
419 // we need to read it now
420 downloadAndReadIndexList(media, dest_dir);
423 if ( _options & AutoAddContentFileIndexes )
425 // only try to add an index if it exists
426 filesystem::DirEntry contentfile;
427 contentfile.name = "content"; contentfile.type = filesystem::FT_FILE;
428 if ( find( content.begin(), content.end(), contentfile ) != content.end() )
430 // add the index of this directory
431 OnMediaLocation indexloc(resource);
432 indexloc.changeFilename(resource.filename() + "content");
434 // we need to read it now
435 downloadAndReadIndexList(media, dest_dir);
440 void Fetcher::Impl::getDirectoryContent( MediaSetAccess &media,
441 const OnMediaLocation &resource,
442 filesystem::DirContent &content )
444 if ( _dircontent.find(resource.filename().asString())
445 != _dircontent.end() )
447 filesystem::DirContent filled(_dircontent[resource.filename().asString()]);
449 std::copy(filled.begin(), filled.end(), std::back_inserter(content));
453 filesystem::DirContent tofill;
454 media.dirInfo( tofill,
458 std::copy(tofill.begin(), tofill.end(), std::back_inserter(content));
459 _dircontent[resource.filename().asString()] = tofill;
463 void Fetcher::Impl::addDirJobs( MediaSetAccess &media,
464 const OnMediaLocation &resource,
465 const Pathname &dest_dir, FetcherJob::Flags flags )
467 // first get the content of the directory so we can add
468 // individual transfer jobs
469 MIL << "Adding directory " << resource.filename() << endl;
470 filesystem::DirContent content;
471 getDirectoryContent(media, resource, content);
473 // this method test for the option flags so indexes are added
474 // only if the options are enabled
475 autoaddIndexes(content, media, resource, dest_dir);
477 for ( filesystem::DirContent::const_iterator it = content.begin();
481 // skip SHA1SUMS* as they were already retrieved
482 if ( str::hasPrefix(it->name, "SHA1SUMS") )
485 Pathname filename = resource.filename() + it->name;
489 case filesystem::FT_NOT_AVAIL: // old directory.yast contains no typeinfo at all
490 case filesystem::FT_FILE:
492 CheckSum chksm(resource.checksum());
493 if ( _checksums.find(filename.asString()) != _checksums.end() )
495 // the checksum can be replaced with the one in the index.
496 chksm = _checksums[filename.asString()];
497 //MIL << "resource " << filename << " has checksum in the index file." << endl;
500 WAR << "Resource " << filename << " has no checksum in the index either." << endl;
502 if ( flags & FetcherJob::AlwaysVerifyChecksum )
503 enqueueDigested(OnMediaLocation(filename, resource.medianr()).setChecksum(chksm));
505 enqueue(OnMediaLocation(filename, resource.medianr()).setChecksum(chksm));
508 case filesystem::FT_DIR: // newer directory.yast contain at least directory info
509 if ( flags & FetcherJob::Recursive )
510 addDirJobs(media, filename, dest_dir, flags);
513 // don't provide devices, sockets, etc.
519 void Fetcher::Impl::provideToDest( MediaSetAccess &media, const OnMediaLocation &resource, const Pathname &dest_dir )
521 bool got_from_cache = false;
523 // start look in cache
524 got_from_cache = provideFromCache(resource, dest_dir);
526 if ( ! got_from_cache )
528 MIL << "Not found in cache, downloading" << endl;
530 // try to get the file from the net
533 Pathname tmp_file = media.provideFile(resource);
534 Pathname dest_full_path = dest_dir + resource.filename();
535 if ( assert_dir( dest_full_path.dirname() ) != 0 )
536 ZYPP_THROW( Exception("Can't create " + dest_full_path.dirname().asString()));
537 if ( filesystem::copy(tmp_file, dest_full_path ) != 0 )
539 if ( ! PathInfo(tmp_file).isExist() )
540 ERR << tmp_file << " does not exist" << endl;
541 if ( ! PathInfo(dest_full_path.dirname()).isExist() )
542 ERR << dest_full_path.dirname() << " does not exist" << endl;
544 media.releaseFile(resource); //not needed anymore, only eat space
545 ZYPP_THROW( Exception("Can't copy " + tmp_file.asString() + " to " + dest_dir.asString()));
548 media.releaseFile(resource); //not needed anymore, only eat space
550 catch (Exception & excpt_r)
552 ZYPP_CAUGHT(excpt_r);
553 excpt_r.remember("Can't provide " + resource.filename().asString() + " : " + excpt_r.msg());
555 if ( resource.optional() )
557 WAR << "optional resource " << resource << " could not be transfered" << endl;
562 ZYPP_RETHROW(excpt_r);
568 // We got the file from cache
569 // continue with next file
574 // helper class to consume a content file
575 struct ContentReaderHelper : public parser::susetags::ContentFileReader
577 ContentReaderHelper()
579 setRepoIndexConsumer( bind( &ContentReaderHelper::consumeIndex, this, _1 ) );
582 void consumeIndex( const parser::susetags::RepoIndex_Ptr & data_r )
583 { _repoindex = data_r; }
585 parser::susetags::RepoIndex_Ptr _repoindex;
588 // generic function for reading indexes
589 void Fetcher::Impl::readIndex( const Pathname &index, const Pathname &basedir )
591 if ( index.basename() == "SHA1SUMS" )
592 readSha1sumsIndex(index, basedir);
593 else if ( index.basename() == "content" )
594 readContentFileIndex(index, basedir);
596 WAR << index << ": index file format not known" << endl;
599 // reads a content file index
600 void Fetcher::Impl::readContentFileIndex( const Pathname &index, const Pathname &basedir )
602 ContentReaderHelper reader;
604 MIL << index << " contains " << reader._repoindex->mediaFileChecksums.size() << " checksums." << endl;
605 for_( it, reader._repoindex->mediaFileChecksums.begin(), reader._repoindex->mediaFileChecksums.end() )
607 // content file entries don't start with /
608 _checksums[(basedir + it->first).asString()] = it->second;
612 // reads a SHA1SUMS file index
613 void Fetcher::Impl::readSha1sumsIndex( const Pathname &index, const Pathname &basedir )
615 std::ifstream in( index.c_str() );
619 while ( getline(in, buffer) )
621 vector<string> words;
622 str::split( buffer, back_inserter(words) );
623 if ( words.size() != 2 )
624 ZYPP_THROW(Exception("Wrong format for SHA1SUMS file"));
625 //MIL << "check: '" << words[0] << "' | '" << words[1] << "'" << endl;
626 if ( ! words[1].empty() )
627 _checksums[(basedir + words[1]).asString()] = CheckSum::sha1(words[0]);
631 ZYPP_THROW(Exception("Can't open SHA1SUMS file: " + index.asString()));
634 void Fetcher::Impl::downloadIndex( MediaSetAccess &media, const OnMediaLocation &resource, const Pathname &dest_dir)
636 MIL << "downloading index " << resource << endl;
638 // create a new fetcher with a different state to transfer the
639 // file containing checksums and its signature
641 // signature checker for index. We havent got the signature from
643 SignatureFileChecker sigchecker;
645 // build the name of the index and the signature
646 OnMediaLocation idxloc(resource);
647 OnMediaLocation sigloc(resource);
648 OnMediaLocation keyloc(resource);
650 // we should not fail the download if those don't exists
651 // the checking will warn later
652 sigloc.setOptional(true);
653 keyloc.setOptional(true);
655 // calculate signature and key name
656 sigloc.changeFilename( sigloc.filename().extend(".asc") );
657 keyloc.changeFilename( keyloc.filename().extend(".key") );
659 //assert_dir(dest_dir + idxloc.filename().dirname());
661 // transfer the signature
662 fetcher.enqueue(sigloc);
663 fetcher.start( dest_dir, media );
664 // if we get the signature, update the checker
665 if ( PathInfo(dest_dir + sigloc.filename()).isExist() )
666 sigchecker = SignatureFileChecker(dest_dir + sigloc.filename());
671 fetcher.enqueue(keyloc);
672 fetcher.start( dest_dir, media );
675 // now the index itself
676 fetcher.enqueue( idxloc, FileChecker(sigchecker) );
677 fetcher.start( dest_dir, media );
681 // this method takes all the user pointed indexes, gets them and also tries to
682 // download their signature, and verify them. After that, its parses each one
683 // to fill the checksum cache.
684 void Fetcher::Impl::downloadAndReadIndexList( MediaSetAccess &media, const Pathname &dest_dir)
686 // if there is no indexes, then just return to avoid
687 // the directory listing
688 if ( _indexes.empty() )
690 MIL << "No indexes to read." << endl;
694 for ( list<FetcherIndex_Ptr>::const_iterator it_idx = _indexes.begin();
695 it_idx != _indexes.end(); ++it_idx )
697 downloadIndex( media, (*it_idx)->location, dest_dir );
698 // now we have the indexes in dest_dir
699 readIndex( dest_dir + (*it_idx)->location.filename(), (*it_idx)->location.filename().dirname() );
701 MIL << "done reading indexes" << endl;
704 // start processing all fetcher jobs.
705 // it processes any user pointed index first
706 void Fetcher::Impl::start( const Pathname &dest_dir,
707 MediaSetAccess &media,
708 const ProgressData::ReceiverFnc & progress_receiver )
710 ProgressData progress(_resources.size());
711 progress.sendTo(progress_receiver);
713 downloadAndReadIndexList(media, dest_dir);
715 for ( list<FetcherJob_Ptr>::const_iterator it_res = _resources.begin(); it_res != _resources.end(); ++it_res )
718 if ( (*it_res)->flags & FetcherJob::Directory )
720 const OnMediaLocation location((*it_res)->location);
721 addDirJobs(media, location, dest_dir, (*it_res)->flags);
725 // may be this code can be factored out
726 // together with the autodiscovery of indexes
728 if ( ( _options & AutoAddSha1sumsIndexes ) ||
729 ( _options & AutoAddContentFileIndexes ) )
731 // if auto indexing is enabled, then we need to read the
732 // index for each file. We look only in the directory
733 // where the file is. this is expensive of course.
734 filesystem::DirContent content;
735 getDirectoryContent(media, (*it_res)->location.filename().dirname(), content);
736 // this method test for the option flags so indexes are added
737 // only if the options are enabled
738 MIL << "Autodiscovering signed indexes on '"
739 << (*it_res)->location.filename().dirname() << "' for '"
740 << (*it_res)->location.filename() << "'" << endl;
742 autoaddIndexes(content, media, (*it_res)->location.filename().dirname(), dest_dir);
744 // also look in the root of the media
746 getDirectoryContent(media, Pathname("/"), content);
747 // this method test for the option flags so indexes are added
748 // only if the options are enabled
749 MIL << "Autodiscovering signed indexes on '"
751 << (*it_res)->location.filename() << "'" << endl;
753 autoaddIndexes(content, media, Pathname("/"), dest_dir);
756 provideToDest(media, (*it_res)->location, dest_dir);
758 // if the file was not transfered, and no exception, just
759 // return, as it was an optional file
760 if ( ! PathInfo(dest_dir + (*it_res)->location.filename()).isExist() )
763 // if the checksum is empty, but the checksum is in one of the
764 // indexes checksum, then add a checker
765 if ( (*it_res)->location.checksum().empty() )
767 if ( _checksums.find((*it_res)->location.filename().asString())
768 != _checksums.end() )
770 CheckSum chksm = _checksums[(*it_res)->location.filename().asString()];
771 ChecksumFileChecker digest_check(chksm);
772 (*it_res)->checkers.push_back(digest_check);
776 // if the index checksum is empty too, we only add the checker
777 // if the AlwaysVerifyChecksum option is set on
778 if ( (*it_res)->flags & FetcherJob::AlwaysVerifyChecksum )
780 // add the checker with the empty checksum
781 ChecksumFileChecker digest_check((*it_res)->location.checksum());
782 (*it_res)->checkers.push_back(digest_check);
788 // checksum is not empty, so add a checksum checker
789 ChecksumFileChecker digest_check((*it_res)->location.checksum());
790 (*it_res)->checkers.push_back(digest_check);
793 // validate job, this throws if not valid
794 validate((*it_res)->location, dest_dir, (*it_res)->checkers);
796 if ( ! progress.incr() )
797 ZYPP_THROW(AbortRequestException());
801 /** \relates Fetcher::Impl Stream output */
802 inline std::ostream & operator<<( std::ostream & str, const Fetcher::Impl & obj )
804 for ( list<FetcherJob_Ptr>::const_iterator it_res = obj._resources.begin(); it_res != obj._resources.end(); ++it_res )
812 : _pimpl( new Impl() )
818 void Fetcher::setOptions( Fetcher::Options options )
820 _pimpl->setOptions(options);
823 Fetcher::Options Fetcher::options() const
825 return _pimpl->options();
828 void Fetcher::enqueueDigested( const OnMediaLocation &resource, const FileChecker &checker )
830 _pimpl->enqueueDigested(resource, checker);
833 void Fetcher::enqueueDir( const OnMediaLocation &resource,
835 const FileChecker &checker )
837 _pimpl->enqueueDir(resource, recursive, checker);
840 void Fetcher::enqueueDigestedDir( const OnMediaLocation &resource,
842 const FileChecker &checker )
844 _pimpl->enqueueDigestedDir(resource, recursive, checker);
848 void Fetcher::addIndex( const OnMediaLocation &resource )
850 _pimpl->addIndex(resource);
854 void Fetcher::enqueue( const OnMediaLocation &resource, const FileChecker &checker )
856 _pimpl->enqueue(resource, checker);
859 void Fetcher::addCachePath( const Pathname &cache_dir )
861 _pimpl->addCachePath(cache_dir);
864 void Fetcher::reset()
869 void Fetcher::start( const Pathname &dest_dir,
870 MediaSetAccess &media,
871 const ProgressData::ReceiverFnc & progress_receiver )
873 _pimpl->start(dest_dir, media, progress_receiver);
876 std::ostream & operator<<( std::ostream & str, const Fetcher & obj )
878 return str << *obj._pimpl;
881 /////////////////////////////////////////////////////////////////
883 ///////////////////////////////////////////////////////////////////