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