1 /*
2  * ebusd - daemon for communication with eBUS heating systems.
3  * Copyright (C) 2014-2021 John Baier <ebusd@ebusd.eu>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "lib/ebus/filereader.h"
20 #include <sys/stat.h>
21 #include <iostream>
22 #include <string>
23 #include <vector>
24 #include <iomanip>
25 #include <climits>
26 #include <fstream>
27 #include <functional>
28 
29 namespace ebusd {
30 
31 using std::ifstream;
32 using std::ostringstream;
33 using std::cout;
34 using std::endl;
35 using std::setw;
36 using std::dec;
37 
38 
openFile(const string & filename,string * errorDescription,time_t * time)39 istream* FileReader::openFile(const string& filename, string* errorDescription, time_t* time) {
40   struct stat st;
41   if (stat(filename.c_str(), &st) != 0) {
42     *errorDescription = filename;
43     return nullptr;
44   }
45   if (S_ISDIR(st.st_mode)) {
46     *errorDescription = filename+" is a directory";
47     return nullptr;
48   }
49   ifstream* stream = new ifstream();
50   stream->open(filename.c_str(), ifstream::in);
51   if (!stream->is_open()) {
52     *errorDescription = filename;
53     delete(stream);
54     return nullptr;
55   }
56   if (time) {
57     *time = st.st_mtime;
58   }
59   return stream;
60 }
61 
readFromStream(istream * stream,const string & filename,const time_t & mtime,bool verbose,map<string,string> * defaults,string * errorDescription,bool replace,size_t * hash,size_t * size)62 result_t FileReader::readFromStream(istream* stream, const string& filename, const time_t& mtime, bool verbose,
63     map<string, string>* defaults, string* errorDescription, bool replace, size_t* hash, size_t* size) {
64   if (hash) {
65     *hash = 0;
66   }
67   if (size) {
68     *size = 0;
69   }
70   unsigned int lineNo = 0;
71   vector<string> row;
72   result_t result = RESULT_OK;
73   while (stream->peek() != EOF && result == RESULT_OK) {
74     result = readLineFromStream(stream, filename, verbose, &lineNo, &row, errorDescription, replace, hash, size);
75   }
76   return result;
77 }
78 
readLineFromStream(istream * stream,const string & filename,bool verbose,unsigned int * lineNo,vector<string> * row,string * errorDescription,bool replace,size_t * hash,size_t * size)79 result_t FileReader::readLineFromStream(istream* stream, const string& filename, bool verbose,
80     unsigned int* lineNo, vector<string>* row, string* errorDescription, bool replace, size_t* hash, size_t* size) {
81   result_t result;
82   if (!splitFields(stream, row, lineNo, hash, size)) {
83     *errorDescription = "blank line";
84     result = RESULT_ERR_EOF;
85   } else {
86     *errorDescription = "";
87     result = addFromFile(filename, *lineNo, row, errorDescription, replace);
88   }
89   if (result != RESULT_OK) {
90     if (!errorDescription->empty()) {
91       string error;
92       formatError(filename, *lineNo, result, *errorDescription, &error);
93       *errorDescription = error;
94       if (verbose) {
95         cout << error << endl;
96       }
97     } else if (!verbose) {
98       return formatError(filename, *lineNo, result, "", errorDescription);
99     }
100   } else if (!verbose) {
101     *errorDescription = "";
102   }
103   return result;
104 }
105 
trim(string * str)106 void FileReader::trim(string* str) {
107   size_t pos = str->find_first_not_of(" \t");
108   if (pos != string::npos) {
109     str->erase(0, pos);
110   }
111   pos = str->find_last_not_of(" \t");
112   if (pos != string::npos) {
113     str->erase(pos+1);
114   }
115 }
116 
tolower(string * str)117 void FileReader::tolower(string* str) {
118   transform(str->begin(), str->end(), str->begin(), ::tolower);
119 }
120 
hashFunction(const string & str)121 static size_t hashFunction(const string& str) {
122   size_t hash = 0;
123   for (unsigned char c : str) {
124     hash = (31 * hash) ^ c;
125   }
126   return hash;
127 }
128 
splitFields(istream * stream,vector<string> * row,unsigned int * lineNo,size_t * hash,size_t * size)129 bool FileReader::splitFields(istream* stream, vector<string>* row, unsigned int* lineNo,
130     size_t* hash, size_t* size) {
131   row->clear();
132   string line;
133   bool quotedText = false, wasQuoted = false;
134   ostringstream field;
135   char prev = FIELD_SEPARATOR;
136   bool empty = true, read = false;
137   while (getline(*stream, line)) {
138     read = true;
139     ++(*lineNo);
140     trim(&line);
141     size_t length = line.size();
142     if (size) {
143       *size += length + 1;  // normalized with trailing endl
144     }
145     if (hash) {
146       *hash ^= (hashFunction(line) ^ (length << (7 * (*lineNo % 5)))) & 0xffffffff;
147     }
148     if (!quotedText && (length == 0 || line[0] == '#' || (line.length() > 1 && line[0] == '/' && line[1] == '/'))) {
149       if (*lineNo == 1) {
150         break;  // keep empty first line for applying default header
151       }
152       continue;  // skip empty lines and comments
153     }
154     for (size_t pos = 0; pos < length; pos++) {
155       char ch = line[pos];
156       switch (ch) {
157       case FIELD_SEPARATOR:
158         if (quotedText) {
159           field << ch;
160         } else {
161           string str = field.str();
162           trim(&str);
163           empty &= str.empty();
164           row->push_back(str);
165           field.str("");
166           wasQuoted = false;
167         }
168         break;
169       case TEXT_SEPARATOR:
170         if (prev == TEXT_SEPARATOR && !quotedText) {  // double dquote
171           field << ch;
172           quotedText = true;
173         } else if (quotedText) {
174           quotedText = false;
175         } else if (prev == FIELD_SEPARATOR) {
176           quotedText = wasQuoted = true;
177         } else {
178           field << ch;
179         }
180         break;
181       case '\r':
182         break;
183       default:
184         if (prev == TEXT_SEPARATOR && !quotedText && wasQuoted) {
185           field << TEXT_SEPARATOR;  // single dquote in the middle of formerly quoted text
186           quotedText = true;
187         } else if (quotedText && pos == 0 && field.tellp() > 0 && *(field.str().end()-1) != VALUE_SEPARATOR) {
188           field << VALUE_SEPARATOR;  // add separator in between multiline field parts
189         }
190         field << ch;
191         break;
192       }
193       prev = ch;
194     }
195     if (!quotedText) {
196       break;
197     }
198   }
199   string str = field.str();
200   trim(&str);
201   if (empty && str.empty()) {
202     row->clear();
203     return read;
204   }
205   row->push_back(str);
206   return true;
207 }
208 
formatError(const string & filename,unsigned int lineNo,result_t result,const string & error,string * errorDescription)209 result_t FileReader::formatError(const string& filename, unsigned int lineNo, result_t result,
210     const string& error, string* errorDescription) {
211   ostringstream str;
212   if (!errorDescription->empty()) {
213     str << *errorDescription << ", ";
214   }
215   str << filename << ":" << lineNo << ": " << getResultCode(result);
216   if (!error.empty()) {
217     str << ", " << error;
218   }
219   *errorDescription = str.str();
220   return result;
221 }
222 
223 
normalizeLanguage(const string & lang)224 const string MappedFileReader::normalizeLanguage(const string& lang) {
225   string normLang = lang;
226   tolower(&normLang);
227   if (normLang.size() > 2) {
228     size_t pos = normLang.find('.');
229     if (pos == string::npos) {
230       pos = normLang.size();
231     }
232     size_t strip = normLang.find('_');
233     if (strip == string::npos || strip > pos) {
234       strip = pos;
235     }
236     if (strip > 2) {
237       strip = 2;
238     }
239     return normLang.substr(0, strip);
240   }
241   return normLang;
242 }
243 
readFromStream(istream * stream,const string & filename,const time_t & mtime,bool verbose,map<string,string> * defaults,string * errorDescription,bool replace,size_t * hash,size_t * size)244 result_t MappedFileReader::readFromStream(istream* stream, const string& filename, const time_t& mtime, bool verbose,
245     map<string, string>* defaults, string* errorDescription, bool replace, size_t* hash, size_t* size) {
246   m_mutex.lock();
247   m_columnNames.clear();
248   m_lastDefaults.clear();
249   m_lastSubDefaults.clear();
250   if (defaults) {
251     m_lastDefaults[""] = *defaults;
252   }
253   size_t lastSep = filename.find_last_of('/');
254   string defaultsPart = lastSep == string::npos ? filename : filename.substr(lastSep+1);
255   extractDefaultsFromFilename(defaultsPart, &m_lastDefaults[""]);
256   result_t result
257   = FileReader::readFromStream(stream, filename, mtime, verbose, defaults, errorDescription, replace, hash, size);
258   m_mutex.unlock();
259   return result;
260 }
261 
addFromFile(const string & filename,unsigned int lineNo,vector<string> * row,string * errorDescription,bool replace)262 result_t MappedFileReader::addFromFile(const string& filename, unsigned int lineNo, vector<string>* row,
263     string* errorDescription, bool replace) {
264   result_t result;
265   if (lineNo == 1) {  // first line defines column names
266     result = getFieldMap(m_preferLanguage, row, errorDescription);
267     if (result != RESULT_OK) {
268       return result;
269     }
270     if (row->empty()) {
271       *errorDescription = "missing field map";
272       return RESULT_ERR_EOF;
273     }
274     m_columnNames = *row;
275     return RESULT_OK;
276   }
277   if (row->empty()) {
278     return RESULT_OK;
279   }
280   if (m_columnNames.empty()) {
281     *errorDescription = "missing field map";
282     return RESULT_ERR_INVALID_ARG;
283   }
284   map<string, string> rowMapped;
285   vector< map<string, string> > subRowsMapped;
286   bool isDefault = m_supportsDefaults && !(*row)[0].empty() && (*row)[0][0] == '*';
287   if (isDefault) {
288     (*row)[0].erase(0, 1);
289   }
290   size_t lastRepeatStart = UINT_MAX;
291   map<string, string>* lastMappedRow = &rowMapped;
292   bool empty = true;
293   for (size_t colIdx = 0, colNameIdx = 0; colIdx < row->size(); colIdx++, colNameIdx++) {
294     if (colNameIdx >= m_columnNames.size()) {
295       if (lastRepeatStart == UINT_MAX) {
296         *errorDescription = "named columns exceeded";
297         return RESULT_ERR_INVALID_ARG;
298       }
299       colNameIdx = lastRepeatStart;
300     }
301     string columnName = m_columnNames[colNameIdx];
302     if (!columnName.empty() && columnName[0] == '*') {  // marker for next entry
303       if (empty) {
304         lastMappedRow->clear();
305       }
306       if (!empty || lastMappedRow == &rowMapped) {
307         subRowsMapped.resize(subRowsMapped.size() + 1);
308         lastMappedRow = &subRowsMapped[subRowsMapped.size() - 1];
309       }
310       columnName = columnName.substr(1);
311       lastRepeatStart = colNameIdx;
312       empty = true;
313     } else if (columnName == SKIP_COLUMN) {
314       continue;
315     }
316     string value = (*row)[colIdx];
317     empty &= value.empty();
318     (*lastMappedRow)[columnName] = value;
319   }
320   if (empty) {
321     lastMappedRow->clear();
322     if (lastMappedRow != &rowMapped) {
323       subRowsMapped.resize(subRowsMapped.size() - 1);
324     }
325   }
326   if (isDefault) {
327     return addDefaultFromFile(filename, lineNo, &rowMapped, &subRowsMapped, errorDescription);
328   }
329   return addFromFile(filename, lineNo, &rowMapped, &subRowsMapped, errorDescription, replace);
330 }
331 
combineRow(const map<string,string> & row)332 const string MappedFileReader::combineRow(const map<string, string>& row) {
333   ostringstream ostream;
334   bool first = true;
335   for (auto entry : row) {
336     if (first) {
337       first = false;
338     } else {
339       ostream << ", ";
340     }
341     ostream << entry.first << ": \"" << entry.second << "\"";
342   }
343   return ostream.str();
344 }
345 
346 }  // namespace ebusd
347