1 /* bzflag
2  * Copyright (c) 1993-2021 Tim Riker
3  *
4  * This package is free software;  you can redistribute it and/or
5  * modify it under the terms of the license found in the file
6  * named COPYING that should have accompanied this file.
7  *
8  * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
9  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
10  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
11  */
12 
13 #include "common.h"
14 
15 // implementation header
16 #include "CacheManager.h"
17 
18 // system headers
19 #include <string>
20 #include <vector>
21 #include <iostream>
22 #include <algorithm>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <time.h>
27 #include <sys/types.h>
28 #include <sys/stat.h>
29 #ifndef _WIN32
30 #  include <unistd.h>
31 #endif
32 
33 // common headers
34 #include "md5.h"
35 #include "bzfio.h"
36 #include "TextUtils.h"
37 #include "FileManager.h"
38 #include "StateDatabase.h"
39 #include "DirectoryNames.h"
40 
41 
42 // function prototypes
43 static bool fileExists(const std::string& name);
44 static void removeDirs(const std::string& path);
45 static void removeNewlines(char* c);
46 static std::string partialEncoding(const std::string& string);
47 static bool compareUsedDate(const CacheManager::CacheRecord& a,
48                             const CacheManager::CacheRecord& b);
49 
50 
51 CacheManager CACHEMGR;
52 
53 
CacheManager()54 CacheManager::CacheManager() : indexName("CacheIndex.txt")
55 {
56 }
57 
58 
~CacheManager()59 CacheManager::~CacheManager()
60 {
61 }
62 
63 
getIndexPath()64 std::string CacheManager::getIndexPath()
65 {
66     return getCacheDirName() + indexName;
67 }
68 
69 
isCacheFileType(const std::string & name) const70 bool CacheManager::isCacheFileType(const std::string &name) const
71 {
72     if (strncasecmp(name.c_str(), "http://", 7) == 0)
73         return true;
74     if (strncasecmp(name.c_str(), "ftp://", 6) == 0)
75         return true;
76     return false;
77 }
78 
79 
getLocalName(const std::string & name) const80 std::string CacheManager::getLocalName(const std::string &name) const
81 {
82     std::string local = "";
83     if (strncasecmp(name.c_str(), "http://", 7) == 0)
84     {
85         local = getCacheDirName() + "http/";
86         local += partialEncoding(name.substr(7));
87     }
88     else if (strncasecmp(name.c_str(), "ftp://", 6) == 0)
89     {
90         local = getCacheDirName() + "ftp/";
91         local += partialEncoding(name.substr(6));
92     }
93 #ifdef _WIN32
94     std::replace(local.begin(), local.end(), '/', '\\');
95 #endif
96     return local;
97 }
98 
99 
findURL(const std::string & url,CacheRecord & record)100 bool CacheManager::findURL(const std::string& url, CacheRecord& record)
101 {
102     int pos = findRecord(url);
103     if (pos >= 0)
104     {
105         CacheRecord* rec = &records[pos];
106         rec->usedDate = time(NULL); // update the timestamp
107         record = *rec;
108         return true;
109     }
110     return false;
111 }
112 
113 
addFile(CacheRecord & record,const void * data)114 bool CacheManager::addFile(CacheRecord& record, const void* data)
115 {
116     if (((data == NULL) && (record.size != 0)) || (record.url.size() <= 0))
117         return false;
118 
119     record.name = getLocalName(record.url);
120     std::ostream* out = FILEMGR.createDataOutStream(record.name, true /* binary*/);
121     if (out == NULL)
122         return false;
123 
124     bool replacement = false;
125     CacheRecord* rec = &record;
126 
127     int pos = findRecord(record.url);
128     if (pos >= 0)
129     {
130         records[pos] = record;
131         rec = &records[pos];
132         replacement = true;
133     }
134 
135     out->write((const char *)data, rec->size);
136 
137     rec->usedDate = time(NULL); // update the timestamp
138 
139     MD5 md5;
140     md5.update((const unsigned char *)data, rec->size);
141     md5.finalize();
142     rec->key = md5.hexdigest();
143 
144     if (!replacement)
145         records.push_back(*rec);
146 
147     delete out;
148     return true;
149 }
150 
151 
findRecord(const std::string & url)152 int CacheManager::findRecord(const std::string& url)
153 {
154     for (unsigned int i = 0; i < records.size(); i++)
155     {
156         CacheRecord* rec = &(records[i]);
157         if (url == rec->url)
158             return i;
159     }
160     return -1;
161 }
162 
163 
loadIndex()164 bool CacheManager::loadIndex()
165 {
166     records.clear();
167 
168     FILE* file = fopen(getIndexPath().c_str(), "r");
169     if (file == NULL)
170         return false;
171 
172     char buffer[1024];
173     while (fgets(buffer, 1024, file) != NULL)
174     {
175         removeNewlines(buffer);
176         if ((buffer[0] == '\0') || (buffer[0] == '#'))
177             continue;
178 
179         CacheRecord rec;
180         rec.url = buffer;
181         rec.name = getLocalName(rec.url);
182 
183         if (fgets(buffer, 1024, file) == NULL)
184             break;
185         else
186             removeNewlines(buffer);
187         std::string line = buffer;
188         std::vector<std::string> tokens = TextUtils::tokenize(line, " ");
189         if (tokens.size() != 4)
190         {
191             logDebugMessage(1,"loadCacheIndex (bad line): %s\n", buffer);
192             continue;
193         }
194         rec.size = strtoul(tokens[0].c_str(), NULL, 10);
195         rec.date = strtoul(tokens[1].c_str(), NULL, 10);
196         rec.usedDate = strtoul(tokens[2].c_str(), NULL, 10);
197         rec.key = tokens[3];
198         if (fileExists(rec.name))
199             records.push_back(rec);
200     }
201 
202     fclose(file);
203     return true;
204 }
205 
206 
saveIndex()207 bool CacheManager::saveIndex()
208 {
209     std::sort(records.begin(), records.end(), compareUsedDate);
210 
211     std::string indexPath = getIndexPath();
212     std::string tmpIndexName = indexPath + ".tmp";
213 
214     FILE* file = fopen(tmpIndexName.c_str(), "w");
215     if (file == NULL)
216         return false;
217 
218     const time_t nowTime = time(NULL);
219     fprintf(file, "#\n");
220     fprintf(file, "# BZFlag Cache Index - %s", ctime(&nowTime));
221     fprintf(file, "# <filesize>  <filetime>  <lastused>  <md5check>\n");
222     fprintf(file, "#\n\n");
223 
224     for (unsigned int i = 0; i < records.size(); i++)
225     {
226         const CacheRecord& rec = records[i];
227         fprintf(file, "%s\n%u %llu %llu ", rec.url.c_str(), rec.size, (long long unsigned)rec.date,
228                 (long long unsigned)rec.usedDate);
229         fprintf(file, "%s\n\n", rec.key.c_str());
230     }
231 
232     fclose(file);
233 
234 #ifdef _WIN32
235     // Windows sucks yet again. You can't rename a file to a file that
236     // already exists, you have to remove the existing file first. No
237     // atomic transactions.
238     remove(indexPath.c_str());
239 #endif
240 
241     return (rename(tmpIndexName.c_str(), indexPath.c_str()) == 0);
242 }
243 
244 
limitCacheSize()245 void CacheManager::limitCacheSize()
246 {
247     int maxSize = BZDB.evalInt("maxCacheMB") * 1024 * 1024;
248     if (maxSize < 0)
249         maxSize = 0;
250 
251     int currentSize = 0;
252     for (unsigned int i = 0; i < records.size(); i++)
253         currentSize += records[i].size;
254 
255     std::sort(records.begin(), records.end(), compareUsedDate);
256 
257     while ((currentSize > maxSize) && (records.size() > 0))
258     {
259         CacheManager::CacheRecord& rec = records.back();
260         currentSize -= rec.size;
261         remove(rec.name.c_str());
262         removeDirs(rec.name);
263         records.pop_back();
264     }
265 
266     return;
267 }
268 
269 
getCacheList() const270 std::vector<CacheManager::CacheRecord> CacheManager::getCacheList() const
271 {
272     return records;
273 }
274 
275 
fileExists(const std::string & name)276 static bool fileExists (const std::string& name)
277 {
278     struct stat buf;
279 #ifndef _WIN32
280     return (stat(name.c_str(), &buf) == 0);
281 #else
282     // Windows sucks yet again, if there is a trailing  "\"
283     // at the end of the filename, _stat will return -1.
284     std::string dirname = name;
285     while (dirname.find_last_of('\\') == (dirname.size() - 1))
286         dirname.resize(dirname.size() - 1);
287     return (_stat(dirname.c_str(), (struct _stat *) &buf) == 0);
288 #endif
289 }
290 
291 
removeDirs(const std::string & path)292 static void removeDirs(const std::string& path)
293 {
294     unsigned int minLen = (unsigned int)getConfigDirName().size();
295     std::string tmp = path;
296     while (tmp.size() > minLen)
297     {
298 #ifndef _WIN32
299         unsigned int i = (unsigned int)tmp.find_last_of('/');
300 #else
301         unsigned int i = (unsigned int)tmp.find_last_of('\\');
302 #endif
303         tmp = tmp.substr(0, i);
304         if (remove(tmp.c_str()) != 0)
305             break;
306     }
307     return;
308 }
309 
310 
removeNewlines(char * c)311 static void removeNewlines(char* c)
312 {
313     while (*c != '\0')
314     {
315         if ((*c == '\n') || (*c == '\r'))
316             *c = '\0';
317         c++;
318     }
319     return;
320 }
321 
322 
partialEncoding(const std::string & string)323 static std::string partialEncoding(const std::string& string)
324 {
325     // URL encoding removes the '/' and '.', which is
326     // not acceptable. It is nice to have the directory
327     // structure, and to be able to point and click your
328     // way through it to view ".png"s.
329     std::string tmp;
330     char hex[5];
331     for (unsigned int i = 0; i < string.size(); i++)
332     {
333         const char c = string[i];
334         if (TextUtils::isWhitespace(c))
335             tmp += "%20";
336         else if ((c == '%') || (c == '*') || (c == '?') ||
337                  (c == ':') || (c == '"') || (c == '\\'))
338         {
339             tmp += '%';
340             sprintf(hex, "%-2.2X", c);
341             tmp += hex;
342         }
343         else
344             tmp += c;
345     }
346     return tmp;
347 }
348 
349 
compareUsedDate(const CacheManager::CacheRecord & a,const CacheManager::CacheRecord & b)350 static bool compareUsedDate(const CacheManager::CacheRecord& a,
351                             const CacheManager::CacheRecord& b)
352 {
353     // oldest last
354     return (a.usedDate > b.usedDate);
355 }
356 
357 
358 /*
359  * Local Variables: ***
360  * mode: C ***
361  * tab-width: 8 ***
362  * c-basic-offset: 2 ***
363  * indent-tabs-mode: t ***
364  * End: ***
365  * ex: shiftwidth=2 tabstop=8
366  */
367