1 /*
2  * LibrePCB - Professional EDA for everyone!
3  * Copyright (C) 2013 LibrePCB Developers, see AUTHORS.md for contributors.
4  * https://librepcb.org/
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 /*******************************************************************************
21  *  Includes
22  ******************************************************************************/
23 #include "project.h"
24 
25 #include "boards/board.h"
26 #include "circuit/circuit.h"
27 #include "erc/ercmsglist.h"
28 #include "library/projectlibrary.h"
29 #include "metadata/projectmetadata.h"
30 #include "schematics/schematic.h"
31 #include "schematics/schematiclayerprovider.h"
32 #include "settings/projectsettings.h"
33 
34 #include <librepcb/common/application.h>
35 #include <librepcb/common/exceptions.h>
36 #include <librepcb/common/fileio/directorylock.h>
37 #include <librepcb/common/fileio/fileutils.h>
38 #include <librepcb/common/fileio/sexpression.h>
39 #include <librepcb/common/fileio/versionfile.h>
40 #include <librepcb/common/font/strokefontpool.h>
41 
42 #include <QPrinter>
43 #include <QtCore>
44 
45 /*******************************************************************************
46  *  Namespace
47  ******************************************************************************/
48 namespace librepcb {
49 namespace project {
50 
51 /*******************************************************************************
52  *  Constructors / Destructor
53  ******************************************************************************/
54 
Project(std::unique_ptr<TransactionalDirectory> directory,const QString & filename,bool create)55 Project::Project(std::unique_ptr<TransactionalDirectory> directory,
56                  const QString& filename, bool create)
57   : QObject(nullptr),
58     AttributeProvider(),
59     mDirectory(std::move(directory)),
60     mFilename(filename) {
61   qDebug() << (create ? "create project:" : "open project:")
62            << getFilepath().toNative();
63 
64   // Check if the file extension is correct
65   if (!mFilename.endsWith(".lpp")) {
66     throw RuntimeError(__FILE__, __LINE__,
67                        tr("The suffix of the project file must be \"lpp\"!"));
68   }
69 
70   Version fileFormat = qApp->getFileFormatVersion();
71   if (create) {
72     // Check if there isn't already a project in the selected directory
73     if (mDirectory->fileExists(".librepcb-project") ||
74         mDirectory->fileExists(mFilename)) {
75       throw RuntimeError(
76           __FILE__, __LINE__,
77           QString(
78               tr("The directory \"%1\" already contains a LibrePCB project."))
79               .arg(getPath().toNative()));
80     }
81   } else {
82     // check if the project does exist
83     if (!mDirectory->fileExists(".librepcb-project")) {
84       throw RuntimeError(
85           __FILE__, __LINE__,
86           QString(
87               tr("The directory \"%1\" does not contain a LibrePCB project."))
88               .arg(getPath().toNative()));
89     }
90     if (!mDirectory->fileExists(mFilename)) {
91       throw RuntimeError(
92           __FILE__, __LINE__,
93           tr("The file \"%1\" does not exist.").arg(getFilepath().toNative()));
94     }
95     // check the project's file format version
96     fileFormat =
97         VersionFile::fromByteArray(mDirectory->read(".librepcb-project"))
98             .getVersion();
99     if (fileFormat > qApp->getFileFormatVersion()) {
100       throw RuntimeError(
101           __FILE__, __LINE__,
102           QString(
103               tr("This project was created with a newer application version.\n"
104                  "You need at least LibrePCB %1 to open it.\n\n%2"))
105               .arg(fileFormat.toPrettyStr(3))
106               .arg(getFilepath().toNative()));
107     }
108   }
109 
110   // OK - the project is locked (or read-only) and can be opened!
111   // Until this line, there was no memory allocated on the heap. But in the rest
112   // of the constructor, a lot of object will be created on the heap. If an
113   // exception is thrown somewhere, we must ensure that all the allocated memory
114   // gets freed. This is done by a try/catch block. In the catch-block, all
115   // allocated memory will be freed. Then the exception is rethrown to leave the
116   // constructor.
117 
118   try {
119     // copy and/or load stroke fonts
120     TransactionalDirectory fontobeneDir(*mDirectory, "resources/fontobene");
121     if (create) {
122       FilePath src = qApp->getResourcesFilePath("fontobene");
123       foreach (const FilePath& fp,
124                FileUtils::getFilesInDirectory(src, {"*.bene"})) {
125         if (fp.getSuffix() == "bene") {
126           fontobeneDir.write(fp.getFilename(), FileUtils::readFile(fp));
127         }
128       }
129     }
130     mStrokeFontPool.reset(new StrokeFontPool(fontobeneDir));
131 
132     // Create or load metadata
133     if (create) {
134       QString name = cleanElementName(getFilepath().getCompleteBasename());
135       if (!ElementNameConstraint()(name)) {
136         name = "New Project";  // fallback if the filename is not a valid name
137       }
138       mProjectMetadata.reset(new ProjectMetadata(
139           Uuid::createRandom(), ElementName(name), tr("Unknown"), "v1",
140           QDateTime::currentDateTime(), QDateTime::currentDateTime()));
141     } else {
142       QString fp = "project/metadata.lp";
143       SExpression root =
144           SExpression::parse(mDirectory->read(fp), mDirectory->getAbsPath(fp));
145       mProjectMetadata.reset(new ProjectMetadata(root, fileFormat));
146     }
147 
148     // Create all needed objects
149     connect(mProjectMetadata.data(), &ProjectMetadata::attributesChanged, this,
150             &Project::attributesChanged);
151     mProjectSettings.reset(new ProjectSettings(*this, fileFormat, create));
152     mProjectLibrary.reset(
153         new ProjectLibrary(std::unique_ptr<TransactionalDirectory>(
154             new TransactionalDirectory(*mDirectory, "library"))));
155     mErcMsgList.reset(new ErcMsgList(*this));
156     mCircuit.reset(new Circuit(*this, fileFormat, create));
157 
158     // Load all schematic layers
159     mSchematicLayerProvider.reset(new SchematicLayerProvider(*this));
160 
161     // Load all schematics
162     if (!create) {
163       QString fp = "schematics/schematics.lp";
164       SExpression schRoot =
165           SExpression::parse(mDirectory->read(fp), mDirectory->getAbsPath(fp));
166       foreach (const SExpression& node, schRoot.getChildren("schematic")) {
167         FilePath fp =
168             FilePath::fromRelative(getPath(), node.getChild("@0").getValue());
169         std::unique_ptr<TransactionalDirectory> dir(new TransactionalDirectory(
170             *mDirectory, fp.getParentDir().toRelative(getPath())));
171         Schematic* schematic = new Schematic(*this, std::move(dir), fileFormat);
172         addSchematic(*schematic);
173       }
174       qDebug() << mSchematics.count() << "schematics successfully loaded!";
175     }
176 
177     // Load all boards
178     if (!create) {
179       QString fp = "boards/boards.lp";
180       SExpression brdRoot =
181           SExpression::parse(mDirectory->read(fp), mDirectory->getAbsPath(fp));
182       foreach (const SExpression& node, brdRoot.getChildren("board")) {
183         FilePath fp =
184             FilePath::fromRelative(getPath(), node.getChild("@0").getValue());
185         std::unique_ptr<TransactionalDirectory> dir(new TransactionalDirectory(
186             *mDirectory, fp.getParentDir().toRelative(getPath())));
187         Board* board = new Board(*this, std::move(dir), fileFormat);
188         addBoard(*board);
189       }
190       qDebug() << mBoards.count() << "boards successfully loaded!";
191     }
192 
193     // at this point, the whole circuit with all schematics and boards is
194     // successfully loaded, so the ERC list now contains all the correct ERC
195     // messages. So we can now restore the ignore state of each ERC message from
196     // the file.
197     mErcMsgList->restoreIgnoreState();  // can throw
198 
199     if (create) save();  // write all files to file system
200   } catch (...) {
201     // free the allocated memory in the reverse order of their allocation...
202     foreach (Board* board, mBoards) {
203       try {
204         removeBoard(*board, true);
205       } catch (...) {
206       }
207     }
208     foreach (Schematic* schematic, mSchematics) {
209       try {
210         removeSchematic(*schematic, true);
211       } catch (...) {
212       }
213     }
214     throw;  // ...and rethrow the exception
215   }
216 
217   // project successfully opened! :-)
218   qDebug() << "project successfully loaded!";
219 }
220 
~Project()221 Project::~Project() noexcept {
222   // free the allocated memory in the reverse order of their allocation
223 
224   // delete all boards and schematics (and catch all thrown exceptions)
225   foreach (Board* board, mBoards) {
226     try {
227       removeBoard(*board, true);
228     } catch (...) {
229     }
230   }
231   qDeleteAll(mRemovedBoards);
232   mRemovedBoards.clear();
233   foreach (Schematic* schematic, mSchematics) {
234     try {
235       removeSchematic(*schematic, true);
236     } catch (...) {
237     }
238   }
239   qDeleteAll(mRemovedSchematics);
240   mRemovedSchematics.clear();
241 
242   qDebug() << "closed project:" << getFilepath().toNative();
243 }
244 
245 /*******************************************************************************
246  *  Schematic Methods
247  ******************************************************************************/
248 
getSchematicIndex(const Schematic & schematic) const249 int Project::getSchematicIndex(const Schematic& schematic) const noexcept {
250   return mSchematics.indexOf(const_cast<Schematic*>(&schematic));
251 }
252 
getSchematicByUuid(const Uuid & uuid) const253 Schematic* Project::getSchematicByUuid(const Uuid& uuid) const noexcept {
254   foreach (Schematic* schematic, mSchematics) {
255     if (schematic->getUuid() == uuid) return schematic;
256   }
257   return nullptr;
258 }
259 
getSchematicByName(const QString & name) const260 Schematic* Project::getSchematicByName(const QString& name) const noexcept {
261   foreach (Schematic* schematic, mSchematics) {
262     if (schematic->getName() == name) return schematic;
263   }
264   return nullptr;
265 }
266 
createSchematic(const ElementName & name)267 Schematic* Project::createSchematic(const ElementName& name) {
268   QString dirname = FilePath::cleanFileName(
269       *name, FilePath::ReplaceSpaces | FilePath::ToLowerCase);
270   if (dirname.isEmpty()) {
271     throw RuntimeError(__FILE__, __LINE__,
272                        tr("Invalid schematic name: \"%1\"").arg(*name));
273   }
274   std::unique_ptr<TransactionalDirectory> dir(
275       new TransactionalDirectory(*mDirectory, "schematics/" % dirname));
276   if (dir->fileExists("schematic.lp")) {
277     throw RuntimeError(__FILE__, __LINE__,
278                        tr("The schematic exists already: \"%1\"")
279                            .arg(dir->getAbsPath().toNative()));
280   }
281   return Schematic::create(*this, std::move(dir), name);
282 }
283 
addSchematic(Schematic & schematic,int newIndex)284 void Project::addSchematic(Schematic& schematic, int newIndex) {
285   if ((mSchematics.contains(&schematic)) || (&schematic.getProject() != this)) {
286     throw LogicError(__FILE__, __LINE__);
287   }
288   if (getSchematicByUuid(schematic.getUuid())) {
289     throw RuntimeError(
290         __FILE__, __LINE__,
291         QString("There is already a schematic with the UUID \"%1\"!")
292             .arg(schematic.getUuid().toStr()));
293   }
294   if (getSchematicByName(*schematic.getName())) {
295     throw RuntimeError(__FILE__, __LINE__,
296                        tr("There is already a schematic with the name \"%1\"!")
297                            .arg(*schematic.getName()));
298   }
299 
300   if ((newIndex < 0) || (newIndex > mSchematics.count())) {
301     newIndex = mSchematics.count();
302   }
303 
304   schematic.addToProject();  // can throw
305   mSchematics.insert(newIndex, &schematic);
306 
307   if (mRemovedSchematics.contains(&schematic)) {
308     mRemovedSchematics.removeOne(&schematic);
309   }
310 
311   emit schematicAdded(newIndex);
312   emit attributesChanged();
313 }
314 
removeSchematic(Schematic & schematic,bool deleteSchematic)315 void Project::removeSchematic(Schematic& schematic, bool deleteSchematic) {
316   if ((!mSchematics.contains(&schematic)) ||
317       (mRemovedSchematics.contains(&schematic))) {
318     throw LogicError(__FILE__, __LINE__);
319   }
320   if ((!deleteSchematic) && (!schematic.isEmpty())) {
321     throw RuntimeError(__FILE__, __LINE__,
322                        tr("There are still elements in the schematic \"%1\"!")
323                            .arg(*schematic.getName()));
324   }
325 
326   int index = getSchematicIndex(schematic);
327   Q_ASSERT(index >= 0);
328 
329   schematic.removeFromProject();  // can throw
330   mSchematics.removeAt(index);
331 
332   emit schematicRemoved(index);
333   emit attributesChanged();
334 
335   if (deleteSchematic) {
336     delete &schematic;
337   } else {
338     mRemovedSchematics.append(&schematic);
339   }
340 }
341 
exportSchematicsAsPdf(const FilePath & filepath)342 void Project::exportSchematicsAsPdf(const FilePath& filepath) {
343   // Create output directory first because QPrinter silently fails if it doesn't
344   // exist.
345   FileUtils::makePath(filepath.getParentDir());  // can throw
346 
347   QPrinter printer(QPrinter::HighResolution);
348   printer.setPaperSize(QPrinter::A4);
349   printer.setOrientation(QPrinter::Landscape);
350   printer.setOutputFormat(QPrinter::PdfFormat);
351   printer.setCreator(QString("LibrePCB %1").arg(qApp->applicationVersion()));
352   printer.setOutputFileName(filepath.toStr());
353 
354   QList<int> pages;
355   for (int i = 0; i < mSchematics.count(); i++) pages.append(i);
356 
357   printSchematicPages(printer, pages);
358 }
359 
printSchematicPages(QPrinter & printer,QList<int> & pages)360 void Project::printSchematicPages(QPrinter& printer, QList<int>& pages) {
361   if (pages.isEmpty())
362     throw RuntimeError(__FILE__, __LINE__, tr("No schematic pages selected."));
363 
364   QPainter painter(&printer);
365 
366   for (int i = 0; i < pages.count(); i++) {
367     Schematic* schematic = getSchematicByIndex(pages[i]);
368     if (!schematic) {
369       throw RuntimeError(
370           __FILE__, __LINE__,
371           tr("No schematic page with the index %1 found.").arg(pages[i]));
372     }
373     schematic->clearSelection();
374     schematic->renderToQPainter(painter);
375 
376     if (i != pages.count() - 1) {
377       if (!printer.newPage()) {
378         throw RuntimeError(__FILE__, __LINE__,
379                            tr("Unknown error while printing."));
380       }
381     }
382   }
383 }
384 
385 /*******************************************************************************
386  *  Board Methods
387  ******************************************************************************/
388 
getBoardIndex(const Board & board) const389 int Project::getBoardIndex(const Board& board) const noexcept {
390   return mBoards.indexOf(const_cast<Board*>(&board));
391 }
392 
getBoardByUuid(const Uuid & uuid) const393 Board* Project::getBoardByUuid(const Uuid& uuid) const noexcept {
394   foreach (Board* board, mBoards) {
395     if (board->getUuid() == uuid) return board;
396   }
397   return nullptr;
398 }
399 
getBoardByName(const QString & name) const400 Board* Project::getBoardByName(const QString& name) const noexcept {
401   foreach (Board* board, mBoards) {
402     if (board->getName() == name) return board;
403   }
404   return nullptr;
405 }
406 
createBoard(const ElementName & name)407 Board* Project::createBoard(const ElementName& name) {
408   QString dirname = FilePath::cleanFileName(
409       *name, FilePath::ReplaceSpaces | FilePath::ToLowerCase);
410   if (dirname.isEmpty()) {
411     throw RuntimeError(__FILE__, __LINE__,
412                        tr("Invalid board name: \"%1\"").arg(*name));
413   }
414   std::unique_ptr<TransactionalDirectory> dir(
415       new TransactionalDirectory(*mDirectory, "boards/" % dirname));
416   if (dir->fileExists("board.lp")) {
417     throw RuntimeError(__FILE__, __LINE__,
418                        tr("The board exists already: \"%1\"")
419                            .arg(dir->getAbsPath().toNative()));
420   }
421   return Board::create(*this, std::move(dir), name);
422 }
423 
createBoard(const Board & other,const ElementName & name)424 Board* Project::createBoard(const Board& other, const ElementName& name) {
425   QString dirname = FilePath::cleanFileName(
426       *name, FilePath::ReplaceSpaces | FilePath::ToLowerCase);
427   if (dirname.isEmpty()) {
428     throw RuntimeError(__FILE__, __LINE__,
429                        tr("Invalid board name: \"%1\"").arg(*name));
430   }
431   std::unique_ptr<TransactionalDirectory> dir(
432       new TransactionalDirectory(*mDirectory, "boards/" % dirname));
433   if (dir->fileExists("board.lp")) {
434     throw RuntimeError(__FILE__, __LINE__,
435                        tr("The board exists already: \"%1\"")
436                            .arg(dir->getAbsPath().toNative()));
437   }
438   return new Board(other, std::move(dir), name);
439 }
440 
addBoard(Board & board,int newIndex)441 void Project::addBoard(Board& board, int newIndex) {
442   if ((mBoards.contains(&board)) || (&board.getProject() != this)) {
443     throw LogicError(__FILE__, __LINE__);
444   }
445   if (getBoardByUuid(board.getUuid())) {
446     throw RuntimeError(__FILE__, __LINE__,
447                        QString("There is already a board with the UUID \"%1\"!")
448                            .arg(board.getUuid().toStr()));
449   }
450   if (getBoardByName(*board.getName())) {
451     throw RuntimeError(__FILE__, __LINE__,
452                        tr("There is already a board with the name \"%1\"!")
453                            .arg(*board.getName()));
454   }
455 
456   if ((newIndex < 0) || (newIndex > mBoards.count())) {
457     newIndex = mBoards.count();
458   }
459 
460   board.addToProject();  // can throw
461   mBoards.insert(newIndex, &board);
462 
463   if (mRemovedBoards.contains(&board)) {
464     mRemovedBoards.removeOne(&board);
465   }
466 
467   emit boardAdded(newIndex);
468   emit attributesChanged();
469 }
470 
removeBoard(Board & board,bool deleteBoard)471 void Project::removeBoard(Board& board, bool deleteBoard) {
472   if ((!mBoards.contains(&board)) || (mRemovedBoards.contains(&board))) {
473     throw LogicError(__FILE__, __LINE__);
474   }
475 
476   int index = getBoardIndex(board);
477   Q_ASSERT(index >= 0);
478 
479   board.removeFromProject();  // can throw
480   mBoards.removeAt(index);
481 
482   emit boardRemoved(index);
483   emit attributesChanged();
484 
485   if (deleteBoard) {
486     delete &board;
487   } else {
488     mRemovedBoards.append(&board);
489   }
490 }
491 
492 /*******************************************************************************
493  *  General Methods
494  ******************************************************************************/
495 
save()496 void Project::save() {
497   qDebug() << "Save project files to transactional file system...";
498 
499   // Save version file
500   mDirectory->write(
501       ".librepcb-project",
502       VersionFile(qApp->getFileFormatVersion()).toByteArray());  // can throw
503 
504   // Save *.lpp project file
505   mDirectory->write(mFilename, "LIBREPCB-PROJECT");  // can throw
506 
507   // Save project/metadata.lp
508   mDirectory->write(
509       "project/metadata.lp",
510       mProjectMetadata->serializeToDomElement("librepcb_project_metadata")
511           .toByteArray());  // can throw
512 
513   // Save settings
514   mProjectSettings->save();  // can throw
515 
516   // Save library
517   mProjectLibrary->save();  // can throw
518 
519   // Save circuit
520   mCircuit->save();  // can throw
521 
522   // Save ERC messages list
523   mErcMsgList->save();  // can throw
524 
525   // Save schematics/schematics.lp
526   SExpression schRoot = SExpression::createList("librepcb_schematics");
527   foreach (Schematic* schematic, mSchematics) {
528     schRoot.appendChild("schematic",
529                         schematic->getFilePath().toRelative(getPath()), true);
530   }
531   mDirectory->write("schematics/schematics.lp",
532                     schRoot.toByteArray());  // can throw
533 
534   // Save boards/boards.lp
535   SExpression brdRoot = SExpression::createList("librepcb_boards");
536   foreach (Board* board, mBoards) {
537     brdRoot.appendChild("board", board->getFilePath().toRelative(getPath()),
538                         true);
539   }
540   mDirectory->write("boards/boards.lp", brdRoot.toByteArray());  // can throw
541 
542   // Save all removed schematics (*.lp files)
543   foreach (Schematic* schematic, mRemovedSchematics) {
544     schematic->save();  // can throw
545   }
546   // Save all added schematics (*.lp files)
547   foreach (Schematic* schematic, mSchematics) {
548     schematic->save();  // can throw
549   }
550 
551   // Save all removed boards (*.lp files)
552   foreach (Board* board, mRemovedBoards) {
553     board->save();  // can throw
554   }
555   // Save all added boards (*.lp files)
556   foreach (Board* board, mBoards) {
557     board->save();  // can throw
558   }
559 
560   // update the "last modified datetime" attribute of the project
561   mProjectMetadata->updateLastModified();
562 }
563 
564 /*******************************************************************************
565  *  Inherited from AttributeProvider
566  ******************************************************************************/
567 
getUserDefinedAttributeValue(const QString & key) const568 QString Project::getUserDefinedAttributeValue(const QString& key) const
569     noexcept {
570   if (const auto& attr = mProjectMetadata->getAttributes().find(key)) {
571     return attr->getValueTr(true);
572   } else {
573     return QString();
574   }
575 }
576 
getBuiltInAttributeValue(const QString & key) const577 QString Project::getBuiltInAttributeValue(const QString& key) const noexcept {
578   if (key == QLatin1String("PROJECT")) {
579     return *mProjectMetadata->getName();
580   } else if (key == QLatin1String("PROJECT_DIRPATH")) {
581     return getPath().toNative();
582   } else if (key == QLatin1String("PROJECT_BASENAME")) {
583     return getFilepath().getBasename();
584   } else if (key == QLatin1String("PROJECT_FILENAME")) {
585     return getFilepath().getFilename();
586   } else if (key == QLatin1String("PROJECT_FILEPATH")) {
587     return getFilepath().toNative();
588   } else if (key == QLatin1String("CREATED_DATE")) {
589     return mProjectMetadata->getCreated().date().toString(Qt::ISODate);
590   } else if (key == QLatin1String("CREATED_TIME")) {
591     return mProjectMetadata->getCreated().time().toString(Qt::ISODate);
592   } else if (key == QLatin1String("MODIFIED_DATE")) {
593     return mProjectMetadata->getLastModified().date().toString(Qt::ISODate);
594   } else if (key == QLatin1String("MODIFIED_TIME")) {
595     return mProjectMetadata->getLastModified().time().toString(Qt::ISODate);
596   } else if (key == QLatin1String("AUTHOR")) {
597     return mProjectMetadata->getAuthor();
598   } else if (key == QLatin1String("VERSION")) {
599     return mProjectMetadata->getVersion();
600   } else if (key == QLatin1String("PAGES")) {
601     return QString::number(mSchematics.count());
602   } else if (key == QLatin1String("PAGE_X_OF_Y")) {
603     return "Page {{PAGE}} of {{PAGES}}";  // do not translate this, must be the
604                                           // same for every user!
605   } else {
606     return QString();
607   }
608 }
609 
610 /*******************************************************************************
611  *  Static Methods
612  ******************************************************************************/
613 
isFilePathInsideProjectDirectory(const FilePath & fp)614 bool Project::isFilePathInsideProjectDirectory(const FilePath& fp) noexcept {
615   FilePath parent = fp.getParentDir();
616   if (isProjectDirectory(parent)) {
617     return true;
618   } else if (parent.isValid() && !parent.isRoot()) {
619     return isFilePathInsideProjectDirectory(parent);
620   } else {
621     return false;
622   }
623 }
624 
isProjectFile(const FilePath & file)625 bool Project::isProjectFile(const FilePath& file) noexcept {
626   return file.getSuffix() == "lpp" && file.isExistingFile() &&
627       isProjectDirectory(file.getParentDir());
628 }
629 
isProjectDirectory(const FilePath & dir)630 bool Project::isProjectDirectory(const FilePath& dir) noexcept {
631   return dir.getPathTo(".librepcb-project").isExistingFile();
632 }
633 
getProjectFileFormatVersion(const FilePath & dir)634 Version Project::getProjectFileFormatVersion(const FilePath& dir) {
635   QByteArray content = FileUtils::readFile(dir.getPathTo(".librepcb-project"));
636   VersionFile file = VersionFile::fromByteArray(content);
637   return file.getVersion();
638 }
639 
640 /*******************************************************************************
641  *  End of File
642  ******************************************************************************/
643 
644 }  // namespace project
645 }  // namespace librepcb
646