1 /*
2 FileMgr.h - all file operations
3
4 Copyright 2019-2021 Will Godfrey and others.
5
6 This file is part of yoshimi, which is free software: you can redistribute
7 it and/or modify it under the terms of the GNU Library General Public
8 License as published by the Free Software Foundation; either version 2 of
9 the License, or (at your option) any later version.
10
11 yoshimi is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 FOR A PARTICULAR PURPOSE. See the GNU General Public License (version 2 or
14 later) for more details.
15
16 You should have received a copy of the GNU General Public License along with
17 yoshimi; if not, write to the Free Software Foundation, Inc., 51 Franklin
18 Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 */
20
21 #ifndef FILEMGR_H
22 #define FILEMGR_H
23
24 #include <cerrno>
25 #include <fcntl.h> // this affects error reporting
26 #include <sys/stat.h>
27 #include <sys/types.h>
28 #include <cstring>
29 #include <string>
30 #include <vector>
31 #include <list>
32 #include <iostream>
33 #include <sstream>
34 #include <fstream>
35 #include <dirent.h>
36 #include <unistd.h>
37 #include <stdio.h>
38 #include <zlib.h>
39
40 #include "globals.h"
41
42 #define OUR_PATH_MAX 4096
43 /*
44 * PATH_MAX is a poorly defined constant, and not very
45 * portable. As this function is only used for a simple
46 * tightly defined purpose we set a value to replace it
47 * that should be safe for any reasonable architecture.
48 */
49
50 namespace EXTEN {
51
52 using std::string;
53
54 const string config = ".config";
55 const string instance = ".instance";
56 const string validBank = ".bankdir";
57 const string history = ".history";
58 const string zynInst = ".xiz";
59 const string yoshInst = ".xiy";
60 const string anyInst = ".xi*";
61 const string patchset = ".xmz";
62 const string state = ".state";
63 const string presets = ".xpz";
64 const string scale = ".xsz";
65 const string scalaTuning = ".scl";
66 const string scalaKeymap = ".kbm";
67 const string vector = ".xvy";
68 const string mlearn = ".xly";
69 const string MSwave= ".wav";
70
71 }//(End)namespace EXTEN
72
73
74 namespace file {
75
76 using std::string;
77 using std::stringstream;
78
79 // make a filename legal
make_legit_filename(string & fname)80 inline void make_legit_filename(string& fname)
81 {
82 for (unsigned int i = 0; i < fname.size(); ++i)
83 {
84 char c = fname.at(i);
85 if (!((c >= '0' && c <= '9')
86 || (c >= 'A' && c <= 'Z')
87 || (c >= 'a' && c <= 'z')
88 || c == '-'
89 || c == ' '
90 || c == '.'))
91 fname.at(i) = '_';
92 }
93 }
94
95
96 // make a complete path extra legal
make_legit_pathname(string & fname)97 inline void make_legit_pathname(string& fname)
98 {
99 for (unsigned int i = 0; i < fname.size(); ++i)
100 {
101 char c = fname.at(i);
102 if (!((c >= '0' && c <= '9')
103 || (c >= 'A' && c <= 'Z')
104 || (c >= 'a' && c <= 'z')
105 || c == '-'
106 || c == '/'
107 || c == '.'))
108 fname.at(i) = '_';
109 }
110 }
111
112
113 /*
114 * tries to find build time doc directory
115 * currently only used to find the latest user guide
116 */
localPath(void)117 inline string localPath(void)
118 {
119 char *tmpath;
120 tmpath = (char*) malloc(OUR_PATH_MAX);
121 getcwd (tmpath, OUR_PATH_MAX);
122 string path = string(tmpath);
123 free(tmpath);
124 size_t found = path.rfind("/");
125 if (found != string::npos)
126 path = path.substr(0, found + 1) + "doc";
127 else
128 path = "";
129 return path;
130 }
131
132 /*
133 * this now returns the last modified time which can
134 * never be zero so can represent 'true'
135 */
isRegularFile(const string & chkpath)136 inline size_t isRegularFile(const string& chkpath)
137 {
138 struct stat st;
139 if (!stat(chkpath.c_str(), &st))
140 if (S_ISREG(st.st_mode))
141 return st.st_mtime;
142 return 0;
143 }
144
145
isDirectory(const string & chkpath)146 inline bool isDirectory(const string& chkpath)
147 {
148 struct stat st;
149 if (!stat(chkpath.c_str(), &st))
150 {
151 if (S_ISDIR(st.st_mode))
152 return true;
153 }
154 return false;
155 }
156
157 /* performs specific OS command
158 * optionally returning a multi-line response
159 */
160
cmd2string(std::string cmd)161 inline bool cmd2string(std::string cmd)
162 {
163 FILE* fp = popen(cmd.c_str(), "r");
164 if(fp)
165 {
166 pclose(fp);
167 return true;
168 }
169 return false;
170 }
171
cmd2string(std::string cmd,string & result)172 inline bool cmd2string(std::string cmd, string& result)
173 {
174 bool isok = false;
175 FILE* fp = popen(cmd.c_str(), "r");
176 if(fp)
177 {
178 std::vector<char> buffer(OUR_PATH_MAX);
179 std::size_t n = fread(buffer.data(), 1, buffer.size(), fp);
180 if(n && n < buffer.size())
181 {
182 buffer.data()[n] = 0;
183 result = buffer.data();
184 }
185 pclose(fp);
186 isok = true;
187 }
188 return isok;
189 }
190
191
192 /*
193 * This is only intended for calls on the local filesystem
194 * and to known locations, so buffer size should be adequate
195 * and it avoids dependency on unreliable macros.
196 */
findFile(const string & path,const string & filename,string extension)197 inline string findFile(const string& path, const string& filename, string extension)
198 {
199 if (extension.at(0) != '.')
200 extension = "." + extension;
201 string command = "find " + path + " -name " + filename + extension + " 2>/dev/null -print -quit";
202 //#pragma message "Using '2>/dev/null' here suppresses *all* error messages"
203 // it's done here to suppress warnings of invalid locations
204 FILE *fp = popen(command.c_str(), "r");
205 if (fp == NULL)
206 return "";
207 char line[OUR_PATH_MAX];
208 fscanf(fp,"%[^\n]", line);
209 pclose(fp);
210
211 string fullName(line);
212 unsigned int name_start = fullName.rfind("/") + 1;
213 // Extension might contain a dot, like e.g. '.pdf.gz'
214 unsigned int name_end = fullName.length() - extension.length();
215 fullName = fullName.substr(name_start, name_end - name_start);
216 if (fullName == filename)
217 return line;
218 return "";
219 }
220
findLeafName(const string & name)221 inline string findLeafName(const string& name)
222 {
223 unsigned int name_start;
224 unsigned int name_end;
225
226 name_start = name.rfind("/");
227 name_end = name.rfind(".");
228 return name.substr(name_start + 1, name_end - name_start - 1);
229 }
230
findExtension(const string & name)231 inline string findExtension(const string& name)
232 {
233 size_t point = name.rfind('.');
234 if (point == string::npos)
235 return "";
236 string exten = name.substr(point);
237 if (exten.find('/') != string::npos)
238 return ""; // not acceptible as an extension!
239 return exten;
240 }
241
242
243 // adds or replaces wrong extension with the right one.
setExtension(const string & fname,string ext)244 inline string setExtension(const string& fname, string ext)
245 {
246 if (ext.at(0) != '.')
247 ext = "." + ext;
248 string tmp; // return value
249 size_t ext_pos = fname.rfind('.'); // period, if any
250 size_t slash_pos = fname.rfind('/'); // UNIX path-separator
251 if (slash_pos == string::npos)
252 {
253 // There are no slashes in the string, therefore the last period, if
254 // any, must be at the position of the extension period.
255
256 ext_pos = fname.rfind('.');
257 if (ext_pos == string::npos || ext_pos == 0)
258 {
259 // There is no period, therefore there is no extension.
260 // Append the extension.
261
262 tmp = fname + ext;
263 }
264 else
265 {
266 // Replace everything after the last period.
267
268 tmp = fname.substr(0, ext_pos);
269 tmp += ext;
270 }
271 }
272 else
273 {
274 // If the period precedes the slash, then it is not the extension.
275 // Add the whole extension. Otherwise, replace the extension.
276
277 if (slash_pos > ext_pos)
278 tmp = fname + ext;
279 else
280 {
281 tmp = fname.substr(0, ext_pos);
282 tmp += ext;
283 }
284 }
285 return tmp;
286 }
287
288
copyFile(const string & source,const string & destination,char option)289 inline bool copyFile(const string& source, const string& destination, char option)
290 {
291 // options
292 // 0 = only write if not already present
293 // 1 = always write / overwrite
294 // 2 = only write if newer
295
296 if (option == 0)
297 {
298 if(isRegularFile(destination))
299 {
300 //std::cout << "Not writing " << destination << std::endl;
301 return 0; // counts as a successful write
302 }
303 else
304 {
305 ;//std::cout << "Writing " << destination << std::endl;
306 }
307 }
308
309 if (false)//option != 0 && isRegularFile(destination))
310 std::cout << "Exists " << destination << std::endl;
311
312
313 struct stat sourceInfo;
314 stat(source.c_str(), &sourceInfo);
315 if (option == 2)
316 {
317 if (isRegularFile(destination))
318 {
319 struct stat destInfo;
320 stat(destination.c_str(), &destInfo);
321 if (sourceInfo.st_mtime <= destInfo.st_mtime)
322 {
323 //std::cout << source << " Not newer" << std::endl;
324 return 0; // it's already the newest
325 }
326 }
327 }
328
329 std::ifstream infile (source, std::ios::in|std::ios::binary|std::ios::ate);
330 if (!infile.is_open())
331 return 1;
332 std::ofstream outfile (destination, std::ios::out|std::ios::binary);
333 if (!outfile.is_open())
334 return 1;
335
336 std::streampos size = infile.tellg();
337 char *memblock = new char [size];
338 infile.seekg (0, std::ios::beg);
339 infile.read(memblock, size);
340 infile.close();
341 outfile.write(memblock, size);
342 outfile.close();
343 delete[] memblock;
344
345 if (option == 2)
346 {
347 struct timespec ts[2];
348 ts[1].tv_sec = (sourceInfo.st_mtime % 10000000000);
349 ts[1].tv_nsec = (sourceInfo.st_mtime / 10000000000);
350 utimensat(0, destination.c_str(), ts, 0);
351 }
352 return 0;
353 }
354
355
copyDir(const string & source,const string & destination,char option)356 inline uint32_t copyDir(const string& source, const string& destination, char option)
357 {
358 //std::cout << "source dir " << source << " to " << destination << std::endl;
359 DIR *dir = opendir(source.c_str());
360 if (dir == NULL)
361 return 0xffffffff;
362 struct dirent *fn;
363 int count = 0;
364 int missing = 0;
365 while ((fn = readdir(dir)))
366 {
367 string nextfile = string(fn->d_name);
368 //std::cout << "next file " << nextfile << std::endl;
369 if (!isRegularFile(source + "/" + nextfile))
370 continue;
371 if (nextfile == "." || nextfile == "..")
372 continue;
373 if (copyFile(source + "/" + nextfile, destination + "/" + nextfile, option))
374 ++missing;
375 else
376 ++count;
377 }
378 closedir(dir);
379 return count | (missing << 16);
380 }
381
382 /*
383 * this fills the given list with all contents removing the
384 * directory management from the calling functions.
385 */
listDir(std::list<string> * dirList,const string & dirName)386 inline int listDir(std::list<string>* dirList, const string& dirName)
387 {
388 DIR *dir = opendir(dirName.c_str());
389 if (dir == NULL)
390 return 0xffffffff;
391 struct dirent *fn;
392 int count = 0;
393 while ((fn = readdir(dir)))
394 {
395 string name = fn->d_name;
396 if (!name.empty() && name != "." && name != "..")
397 {
398 dirList->push_back(name);
399 ++count;
400 }
401 }
402 closedir(dir);
403 return count;
404 }
405
406
407 /*
408 * Counts all the objects within the directory.
409 */
410
countDir(const std::string dirName)411 inline int countDir(const std::string dirName)
412 {
413 DIR *dir = opendir(dirName.c_str());
414 if (dir == NULL)
415 return -1;
416 struct dirent *fn;
417 int count = 0;
418 char dir1[2] = {'.', 0};
419 char dir2[3] = {'.', '.', 0};
420 while ((fn = readdir(dir)))
421 {
422 if (fn->d_type == DT_DIR)
423 {
424 if(fn->d_name != dir1 && fn->d_name != dir2)
425 ++ count;
426 }
427 }
428 closedir(dir);
429 return count;
430 }
431
432
433 /*
434 * we return the contents as sorted, sequential lists in directories
435 * and files of the required type as a series of leaf names (as the
436 * root directory is already known). This reduces the size of the
437 * string to a manageable length.
438 * Directories are prefixed to make them easier to identify.
439 */
440 inline void dir2string(string &wanted, string currentDir, string exten, int opt = 0)
441 {
442 // options
443 // &1 allow hidden dirs
444 // &2 allow hidden files
445 // &4 allow wildcards
446 // &8 hide all subdirectories
447 // &16 hide files (just looking for dirs)
448 std::list<string> build;
449 wanted = "";
450 uint32_t found = listDir(&build, currentDir);
451 if (found == 0xffffffff)
452 return;
453
454 if (build.size() > 1)
455 build.sort();
456 if(currentDir.back() != '/')
457 currentDir += '/';
458 string line;
459 if (!(opt & 8))
460 {
461 for (std::list<string>::iterator it = build.begin(); it != build.end(); ++it)
462 { // get directories
463 if ((opt & 1) || string(*it).front() != '.') // no hidden dirs
464 {
465 line = *it;
466 if (line.back() != '/')
467 line += '/';
468 if (isDirectory(currentDir + line))
469 wanted += ("Dir: " + line + "\n");
470 }
471 }
472 }
473 if (opt & 16)
474 {
475 build.clear();
476 return;
477 }
478 bool instype = ((exten == ".xiz") | (exten == ".xiy") | (exten == ".xi*"));
479 string last;
480 last.clear();
481 for (std::list<string>::iterator it = build.begin(); it != build.end(); ++it)
482 { // get files
483 if ((opt & 2) || string(*it).front() != '.') // no hidden files
484 {
485 string next;
486 line = currentDir + *it;
487 if (isRegularFile(line))
488 {
489 next.clear();
490 if ((opt & 4))
491 {
492 next = *it;
493 if (!next.empty())
494 wanted += (next + "\n");
495 }
496 else
497 {
498 if (instype)
499 {
500 if (findExtension(line) == ".xiy" || findExtension(line) == ".xiz")
501 next = *it;
502 }
503 else
504 {
505 if (findExtension(line) == exten)
506 next = *it;
507 }
508
509 // remove the extension, the source knows what it is
510 // and it must exist to have been found!
511 if (!next.empty())
512 {
513 size_t pos = next.rfind('.');
514 next = next.substr(0, pos);
515 // also remove instrument type duplicates
516 if (next != last)
517 {
518 last = next;
519 wanted += (next + "\n");
520 }
521 }
522 }
523 }
524 }
525 }
526 build.clear();
527 }
528
529
saveGzipped(char * data,const string & filename,int compression)530 inline string saveGzipped(char *data, const string& filename, int compression)
531 {
532 char options[10];
533 snprintf(options, 10, "wb%d", compression);
534
535 gzFile gzfile;
536 gzfile = gzopen(filename.c_str(), options);
537 if (gzfile == NULL)
538 return "gzopen() == NULL";
539 gzputs(gzfile, data);
540 gzclose(gzfile);
541 return "";
542 }
543
544
saveData(char * buff,size_t bytes,const string & filename)545 inline ssize_t saveData(char *buff, size_t bytes, const string& filename)
546 {
547 //std::cout << "filename " << filename << std::endl;
548 int writefile = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC,
549 S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
550 if (writefile < 0)
551 {
552 //std::cout << std::strerror(errno) << std::endl;
553 return 0;
554 }
555 ssize_t written = write(writefile, buff, bytes);
556 close (writefile);
557 return written;
558 }
559
560
saveText(const string & text,const string & filename)561 inline bool saveText(const string& text, const string& filename)
562 {
563 FILE *writefile = fopen(filename.c_str(), "w");
564 if (!writefile)
565 return 0;
566
567 fputs(text.c_str(), writefile);
568 fclose (writefile);
569 return 1;
570 }
571
572
loadGzipped(const string & _filename,string * report)573 inline char * loadGzipped(const string& _filename, string * report)
574 {
575 string filename = _filename;
576 char *data = NULL;
577 gzFile gzf = gzopen(filename.c_str(), "rb");
578 if (!gzf)
579 {
580 *report = ("Failed to open file " + filename + " for load: " + string(strerror(errno)));
581 return NULL;
582 }
583 const int bufSize = 4096;
584 char fetchBuf[4097];
585 int this_read;
586 int total_bytes = 0;
587 stringstream readStream;
588 for (bool quit = false; !quit;)
589 {
590 memset(fetchBuf, 0, sizeof(fetchBuf) * sizeof(char));
591 this_read = gzread(gzf, fetchBuf, bufSize);
592 if (this_read > 0)
593 {
594 readStream << fetchBuf;
595 total_bytes += this_read;
596 }
597 else if (this_read < 0)
598 {
599 int errnum;
600 *report = ("Read error in zlib: " + string(gzerror(gzf, &errnum)));
601 if (errnum == Z_ERRNO)
602 *report = ("Filesystem error: " + string(strerror(errno)));
603 quit = true;
604 }
605 else if (total_bytes > 0)
606 {
607 data = new char[total_bytes + 1];
608 if (data)
609 {
610 memset(data, 0, total_bytes + 1);
611 memcpy(data, readStream.str().c_str(), total_bytes);
612 }
613 quit = true;
614 }
615 }
616 gzclose(gzf);
617 //*report = "it looks like we sucessfully loaded" + filename;
618 return data;
619 }
620
621 /*
622 * This is used for text files, preserving individual lines. These can
623 * then be split up by the receiving functions without needing a file
624 * handle, or any knowledge of the file system.
625 */
loadText(const string & filename)626 inline string loadText(const string& filename)
627 {
628 FILE *readfile = fopen(filename.c_str(), "r");
629 if (!readfile)
630 return "";
631
632 string text = "";
633 char line [OUR_PATH_MAX];
634 // no Yoshimi input text lines should get anywhere near this!
635 while (!feof(readfile))
636 {
637 string rawline = "";
638 if (fgets(line , OUR_PATH_MAX , readfile))
639 {
640 rawline = string(line);
641 while (rawline.length() > 0 && rawline[0] < '!')
642 rawline.erase(0, 1);
643 if(rawline.length() > 0)
644 {
645 while (rawline.length() > 0 && rawline[rawline.length() - 1] < ' ')
646 {
647 rawline.pop_back();
648 }
649 text += (rawline + '\n');
650 }
651 }
652 }
653 fclose (readfile);
654 return text;
655 }
656
657
createEmptyFile(const string & filename)658 inline bool createEmptyFile(const string& filename)
659 { // not currently used now
660 std::fstream file;
661 file.open(filename, std::ios::out);
662 if (!file)
663 return false;
664 file.close();
665 return true;
666 }
667
668
createDir(const string & dirname)669 inline bool createDir(const string& dirname)
670 {
671 if (isDirectory(dirname))
672 return false; // don't waste time. it's already here!
673 size_t pos = 1;
674 size_t oldPos = pos;
675 string nextDir;
676 bool failed = false;
677 while (pos != string::npos && failed == false)
678 {
679
680 pos = dirname.find("/", oldPos);
681 if (pos == string::npos)
682 nextDir = dirname;
683 else
684 {
685 nextDir = dirname.substr(0, pos).c_str();
686 oldPos = pos + 1;
687 }
688 if (!isDirectory(nextDir))
689 failed = mkdir(nextDir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
690 }
691 return failed;
692 }
693
694
695 /*
696 * The following two functions are currently identical for
697 * linux but that may not always be true nor possibly other
698 * OSs/filers, so you should always use the correct one.
699 */
deleteFile(const string & filename)700 inline bool deleteFile(const string& filename)
701 {
702 bool isOk = remove(filename.c_str()) == 0;
703 return isOk;
704 }
705
706
deleteDir(const string & filename)707 inline bool deleteDir(const string& filename)
708 {
709 bool isOk = remove(filename.c_str()) == 0;
710 return isOk;
711 }
712
713
714 /*
715 * The following two functions are currently identical for
716 * linux but that may not always be true nor possibly other
717 * OSs/filers, so you should always use the correct one.
718 */
renameFile(const string & oldname,const string & newname)719 inline bool renameFile(const string& oldname, const string& newname)
720 {
721 bool isOk = rename(oldname.c_str(), newname.c_str()) == 0;
722 return isOk;
723 }
724
725
renameDir(const string & oldname,const string & newname)726 inline bool renameDir(const string& oldname, const string& newname)
727 {
728 bool isOk = rename(oldname.c_str(), newname.c_str()) == 0;
729 return isOk;
730 }
731
732 // replace build directory with a different
733 // one in the compilation directory
extendLocalPath(const string & leaf)734 inline string extendLocalPath(const string& leaf)
735 {
736 char *tmpath = getcwd (NULL, 0);
737 if (tmpath == NULL)
738 return "";
739
740 string path(tmpath);
741 free(tmpath);
742 size_t found = path.rfind("yoshimi");
743 if (found == string::npos)
744 return "";
745
746 size_t next = path.find('/', found);
747 if (next == string::npos)
748 return "";
749
750 return path.substr(0, next) + leaf;
751 }
752
userHome(void)753 inline string userHome(void)
754 {
755 string home = string(getenv("HOME"));
756 if (home.empty() || !isDirectory(home))
757 home = string("/tmp");
758 return home + '/';
759 }
760
localDir(void)761 inline string localDir(void)
762 {
763 string local = userHome() + ".local/share/yoshimi";
764 if (!isDirectory(local))
765 {
766 if (createDir(local))
767 local = "";
768 }
769 return local;
770 }
771
configDir(void)772 inline string configDir(void)
773 {
774 string config = userHome() + string(EXTEN::config) + "/" + YOSHIMI;
775 if (!isDirectory(config))
776 {
777 if (createDir(config))
778 config = "";
779 }
780 return config;
781 }
782
783 }//(End)namespace file
784 #endif /*FILEMGR_H*/
785