1 /*
2  *  This file is part of nzbget. See <http://nzbget.net>.
3  *
4  *  Copyright (C) 2004 Sven Henkel <sidddy@users.sourceforge.net>
5  *  Copyright (C) 2007-2016 Andrey Prygunkov <hugbug@users.sourceforge.net>
6  *
7  *  This program is free software; you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License as published by
9  *  the Free Software Foundation; either version 2 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This program is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 
22 #include "nzbget.h"
23 #include "NzbFile.h"
24 #include "Log.h"
25 #include "DownloadInfo.h"
26 #include "Options.h"
27 #include "DiskState.h"
28 #include "Util.h"
29 #include "FileSystem.h"
30 
NzbFile(const char * fileName,const char * category)31 NzbFile::NzbFile(const char* fileName, const char* category) :
32 	m_fileName(fileName)
33 {
34 	debug("Creating NZBFile");
35 
36 	m_nzbInfo = std::make_unique<NzbInfo>();
37 	m_nzbInfo->SetFilename(fileName);
38 	m_nzbInfo->SetCategory(category);
39 	m_nzbInfo->BuildDestDirName();
40 }
41 
LogDebugInfo()42 void NzbFile::LogDebugInfo()
43 {
44 	info(" NZBFile %s", *m_fileName);
45 }
46 
AddArticle(FileInfo * fileInfo,std::unique_ptr<ArticleInfo> articleInfo)47 void NzbFile::AddArticle(FileInfo* fileInfo, std::unique_ptr<ArticleInfo> articleInfo)
48 {
49 	int index = articleInfo->GetPartNumber() - 1;
50 
51 	// make Article-List big enough
52 	if (index >= (int)fileInfo->GetArticles()->size())
53 	{
54 		fileInfo->GetArticles()->resize(index + 1);
55 	}
56 
57 	(*fileInfo->GetArticles())[index] = std::move(articleInfo);
58 }
59 
AddFileInfo(std::unique_ptr<FileInfo> fileInfo)60 void NzbFile::AddFileInfo(std::unique_ptr<FileInfo> fileInfo)
61 {
62 	// calculate file size and delete empty articles
63 
64 	int64 size = 0;
65 	int64 missedSize = 0;
66 	int64 oneSize = 0;
67 	int uncountedArticles = 0;
68 	int missedArticles = 0;
69 	int totalArticles = (int)fileInfo->GetArticles()->size();
70 	int i = 0;
71 	for (ArticleList::iterator it = fileInfo->GetArticles()->begin(); it != fileInfo->GetArticles()->end(); )
72 	{
73 		ArticleInfo* article = (*it).get();
74 		if (!article)
75 		{
76 			fileInfo->GetArticles()->erase(it);
77 			it = fileInfo->GetArticles()->begin() + i;
78 			missedArticles++;
79 			if (oneSize > 0)
80 			{
81 				missedSize += oneSize;
82 			}
83 			else
84 			{
85 				uncountedArticles++;
86 			}
87 		}
88 		else
89 		{
90 			size += article->GetSize();
91 			if (oneSize == 0)
92 			{
93 				oneSize = article->GetSize();
94 			}
95 			it++;
96 			i++;
97 		}
98 	}
99 
100 	if (fileInfo->GetArticles()->empty())
101 	{
102 		return;
103 	}
104 
105 	missedSize += uncountedArticles * oneSize;
106 	size += missedSize;
107 	fileInfo->SetNzbInfo(m_nzbInfo.get());
108 	fileInfo->SetSize(size);
109 	fileInfo->SetRemainingSize(size - missedSize);
110 	fileInfo->SetMissedSize(missedSize);
111 	fileInfo->SetTotalArticles(totalArticles);
112 	fileInfo->SetMissedArticles(missedArticles);
113 	m_nzbInfo->GetFileList()->Add(std::move(fileInfo));
114 }
115 
ParseSubject(FileInfo * fileInfo,bool TryQuotes)116 void NzbFile::ParseSubject(FileInfo* fileInfo, bool TryQuotes)
117 {
118 	// Example subject: some garbage "title" yEnc (10/99)
119 
120 	if (!fileInfo->GetSubject())
121 	{
122 		// Malformed file element without subject. We generate subject using internal element id.
123 		fileInfo->SetSubject(CString::FormatStr("%d", fileInfo->GetId()));
124 	}
125 
126 	// strip the "yEnc (10/99)"-suffix
127 	BString<1024> subject = fileInfo->GetSubject();
128 	char* end = subject + strlen(subject) - 1;
129 	if (*end == ')')
130 	{
131 		end--;
132 		while (strchr("0123456789", *end) && end > subject) end--;
133 		if (*end == '/')
134 		{
135 			end--;
136 			while (strchr("0123456789", *end) && end > subject) end--;
137 			if (end - 6 > subject && !strncmp(end - 6, " yEnc (", 7))
138 			{
139 				end[-6] = '\0';
140 			}
141 		}
142 	}
143 
144 	if (TryQuotes)
145 	{
146 		// try to use the filename in quatation marks
147 		char* p = subject;
148 		char* start = strchr(p, '\"');
149 		if (start)
150 		{
151 			start++;
152 			char* end = strchr(start + 1, '\"');
153 			if (end)
154 			{
155 				int len = (int)(end - start);
156 				char* point = strchr(start + 1, '.');
157 				if (point && point < end)
158 				{
159 					BString<1024> filename;
160 					filename.Set(start, len);
161 					fileInfo->SetFilename(filename);
162 					return;
163 				}
164 			}
165 		}
166 	}
167 
168 	// tokenize subject, considering spaces as separators and quotation
169 	// marks as non separatable token delimiters.
170 	// then take the last token containing dot (".") as a filename
171 
172 	typedef std::vector<CString> TokenList;
173 	TokenList tokens;
174 
175 	// tokenizing
176 	char* p = subject;
177 	char* start = p;
178 	bool quot = false;
179 	while (true)
180 	{
181 		char ch = *p;
182 		bool sep = (ch == '\"') || (!quot && ch == ' ') || (ch == '\0');
183 		if (sep)
184 		{
185 			// end of token
186 			int len = (int)(p - start);
187 			if (len > 0)
188 			{
189 				tokens.emplace_back(start, len);
190 			}
191 			start = p;
192 			if (ch != '\"' || quot)
193 			{
194 				start++;
195 			}
196 			quot = *start == '\"';
197 			if (quot)
198 			{
199 				start++;
200 				char* q = strchr(start, '\"');
201 				if (q)
202 				{
203 					p = q - 1;
204 				}
205 				else
206 				{
207 					quot = false;
208 				}
209 			}
210 		}
211 		if (ch == '\0')
212 		{
213 			break;
214 		}
215 		p++;
216 	}
217 
218 	if (!tokens.empty())
219 	{
220 		// finding the best candidate for being a filename
221 		char* besttoken = tokens.back();
222 		for (TokenList::reverse_iterator it = tokens.rbegin(); it != tokens.rend(); it++)
223 		{
224 			char* s = *it;
225 			char* p = strchr(s, '.');
226 			if (p && (p[1] != '\0'))
227 			{
228 				besttoken = s;
229 				break;
230 			}
231 		}
232 		fileInfo->SetFilename(besttoken);
233 	}
234 	else
235 	{
236 		// subject is empty or contains only separators?
237 		debug("Could not extract Filename from Subject: %s. Using Subject as Filename", fileInfo->GetSubject());
238 		fileInfo->SetFilename(fileInfo->GetSubject());
239 	}
240 }
241 
HasDuplicateFilenames()242 bool NzbFile::HasDuplicateFilenames()
243 {
244 	for (FileList::iterator it = m_nzbInfo->GetFileList()->begin(); it != m_nzbInfo->GetFileList()->end(); it++)
245 	{
246 		FileInfo* fileInfo1 = (*it).get();
247 		int dupe = 1;
248 		for (FileList::iterator it2 = it + 1; it2 != m_nzbInfo->GetFileList()->end(); it2++)
249 		{
250 			FileInfo* fileInfo2 = (*it2).get();
251 			if (!strcmp(fileInfo1->GetFilename(), fileInfo2->GetFilename()) &&
252 				strcmp(fileInfo1->GetSubject(), fileInfo2->GetSubject()))
253 			{
254 				dupe++;
255 			}
256 		}
257 
258 		// If more than two files have the same parsed filename but different subjects,
259 		// this means, that the parsing was not correct.
260 		// in this case we take subjects as filenames to prevent
261 		// false "duplicate files"-alarm.
262 		// It's Ok for just two files to have the same filename, this is
263 		// an often case by posting-errors to repost bad files
264 		if (dupe > 2 || (dupe == 2 && m_nzbInfo->GetFileList()->size() == 2))
265 		{
266 			return true;
267 		}
268 	}
269 
270 	return false;
271 }
272 
273 /**
274  * Generate filenames from subjects and check if the parsing of subject was correct
275  */
BuildFilenames()276 void NzbFile::BuildFilenames()
277 {
278 	for (FileInfo* fileInfo : m_nzbInfo->GetFileList())
279 	{
280 		ParseSubject(fileInfo, true);
281 	}
282 
283 	if (HasDuplicateFilenames())
284 	{
285 		for (FileInfo* fileInfo : m_nzbInfo->GetFileList())
286 		{
287 			ParseSubject(fileInfo, false);
288 		}
289 	}
290 
291 	if (HasDuplicateFilenames())
292 	{
293 		m_nzbInfo->SetManyDupeFiles(true);
294 		for (FileInfo* fileInfo : m_nzbInfo->GetFileList())
295 		{
296 			fileInfo->SetFilename(fileInfo->GetSubject());
297 		}
298 	}
299 }
300 
CalcHashes()301 void NzbFile::CalcHashes()
302 {
303 	RawFileList sortedFiles;
304 
305 	for (FileInfo* fileInfo : m_nzbInfo->GetFileList())
306 	{
307 		sortedFiles.push_back(fileInfo);
308 	}
309 
310 	std::sort(sortedFiles.begin(), sortedFiles.end(),
311 		[](FileInfo* first, FileInfo* second)
312 		{
313 			return strcmp(first->GetFilename(), second->GetFilename()) > 0;
314 		});
315 
316 	uint32 fullContentHash = 0;
317 	uint32 filteredContentHash = 0;
318 	int useForFilteredCount = 0;
319 
320 	for (FileInfo* fileInfo : sortedFiles)
321 	{
322 		// check file extension
323 		bool skip = !fileInfo->GetParFile() &&
324 			Util::MatchFileExt(fileInfo->GetFilename(), g_Options->GetParIgnoreExt(), ",;");
325 
326 		for (ArticleInfo* article: fileInfo->GetArticles())
327 		{
328 			int len = strlen(article->GetMessageId());
329 			fullContentHash = Util::HashBJ96(article->GetMessageId(), len, fullContentHash);
330 			if (!skip)
331 			{
332 				filteredContentHash = Util::HashBJ96(article->GetMessageId(), len, filteredContentHash);
333 				useForFilteredCount++;
334 			}
335 		}
336 	}
337 
338 	// if filtered hash is based on less than a half of files - do not use filtered hash at all
339 	if (useForFilteredCount < (int)sortedFiles.size() / 2)
340 	{
341 		filteredContentHash = 0;
342 	}
343 
344 	m_nzbInfo->SetFullContentHash(fullContentHash);
345 	m_nzbInfo->SetFilteredContentHash(filteredContentHash);
346 }
347 
ProcessFiles()348 void NzbFile::ProcessFiles()
349 {
350 	BuildFilenames();
351 
352 	for (FileInfo* fileInfo : m_nzbInfo->GetFileList())
353 	{
354 		fileInfo->MakeValidFilename();
355 
356 		BString<1024> loFileName = fileInfo->GetFilename();
357 		for (char* p = loFileName; *p; p++) *p = tolower(*p); // convert string to lowercase
358 		bool parFile = strstr(loFileName, ".par2");
359 
360 		m_nzbInfo->SetFileCount(m_nzbInfo->GetFileCount() + 1);
361 		m_nzbInfo->SetTotalArticles(m_nzbInfo->GetTotalArticles() + fileInfo->GetTotalArticles());
362 		m_nzbInfo->SetFailedArticles(m_nzbInfo->GetFailedArticles() + fileInfo->GetMissedArticles());
363 		m_nzbInfo->SetCurrentFailedArticles(m_nzbInfo->GetCurrentFailedArticles() + fileInfo->GetMissedArticles());
364 		m_nzbInfo->SetSize(m_nzbInfo->GetSize() + fileInfo->GetSize());
365 		m_nzbInfo->SetRemainingSize(m_nzbInfo->GetRemainingSize() + fileInfo->GetRemainingSize());
366 		m_nzbInfo->SetFailedSize(m_nzbInfo->GetFailedSize() + fileInfo->GetMissedSize());
367 		m_nzbInfo->SetCurrentFailedSize(m_nzbInfo->GetFailedSize());
368 
369 		fileInfo->SetParFile(parFile);
370 		if (parFile)
371 		{
372 			m_nzbInfo->SetParSize(m_nzbInfo->GetParSize() + fileInfo->GetSize());
373 			m_nzbInfo->SetParFailedSize(m_nzbInfo->GetParFailedSize() + fileInfo->GetMissedSize());
374 			m_nzbInfo->SetParCurrentFailedSize(m_nzbInfo->GetParFailedSize());
375 			m_nzbInfo->SetRemainingParCount(m_nzbInfo->GetRemainingParCount() + 1);
376 		}
377 	}
378 
379 	m_nzbInfo->UpdateMinMaxTime();
380 
381 	CalcHashes();
382 
383 	if (g_Options->GetServerMode())
384 	{
385 		for (FileInfo* fileInfo : m_nzbInfo->GetFileList())
386 		{
387 			g_DiskState->SaveFile(fileInfo);
388 			fileInfo->GetArticles()->clear();
389 		}
390 	}
391 
392 	if (m_password)
393 	{
394 		ReadPassword();
395 	}
396 }
397 
398 /**
399  * Password read using XML-parser may have special characters (such as TAB) stripped.
400  * This function rereads password directly from file to keep all characters intact.
401  */
ReadPassword()402 void NzbFile::ReadPassword()
403 {
404 	DiskFile file;
405 	if (!file.Open(m_fileName, DiskFile::omRead))
406 	{
407 		return;
408 	}
409 
410 	// obtain file size.
411 	file.Seek(0, DiskFile::soEnd);
412 	int size  = (int)file.Position();
413 	file.Seek(0, DiskFile::soSet);
414 
415 	// reading first 4KB of the file
416 
417 	CharBuffer buf(4096);
418 
419 	size = size < 4096 ? size : 4096;
420 
421 	// copy the file into the buffer.
422 	file.Read(buf, size);
423 
424 	file.Close();
425 
426 	buf[size-1] = '\0';
427 
428 	char* metaPassword = strstr(buf, "<meta type=\"password\">");
429 	if (metaPassword)
430 	{
431 		metaPassword += 22; // length of '<meta type="password">'
432 		char* end = strstr(metaPassword, "</meta>");
433 		if (end)
434 		{
435 			*end = '\0';
436 			WebUtil::XmlDecode(metaPassword);
437 			m_password = metaPassword;
438 		}
439 	}
440 }
441 
442 #ifdef WIN32
Parse()443 bool NzbFile::Parse()
444 {
445 	CoInitialize(nullptr);
446 
447 	HRESULT hr;
448 
449 	MSXML::IXMLDOMDocumentPtr doc;
450 	hr = doc.CreateInstance(MSXML::CLSID_DOMDocument);
451 	if (FAILED(hr))
452 	{
453 		return false;
454 	}
455 
456 	// Load the XML document file...
457 	doc->put_resolveExternals(VARIANT_FALSE);
458 	doc->put_validateOnParse(VARIANT_FALSE);
459 	doc->put_async(VARIANT_FALSE);
460 
461 	_variant_t vFilename(*WString(*m_fileName));
462 
463 	// 1. first trying to load via filename without URL-encoding (certain charaters doesn't work when encoded)
464 	VARIANT_BOOL success = doc->load(vFilename);
465 	if (success == VARIANT_FALSE)
466 	{
467 		// 2. now trying filename encoded as URL
468 		char url[2048];
469 		EncodeUrl(m_fileName, url, 2048);
470 		debug("url=\"%s\"", url);
471 		_variant_t vUrl(url);
472 
473 		success = doc->load(vUrl);
474 	}
475 
476 	if (success == VARIANT_FALSE)
477 	{
478 		_bstr_t r(doc->GetparseError()->reason);
479 		const char* errMsg = r;
480 		m_nzbInfo->AddMessage(Message::mkError, BString<1024>("Error parsing nzb-file %s: %s",
481 			FileSystem::BaseFileName(m_fileName), errMsg));
482 		return false;
483 	}
484 
485 	if (!ParseNzb(doc))
486 	{
487 		return false;
488 	}
489 
490 	if (m_nzbInfo->GetFileList()->empty())
491 	{
492 		m_nzbInfo->AddMessage(Message::mkError, BString<1024>(
493 			"Error parsing nzb-file %s: file has no content", FileSystem::BaseFileName(m_fileName)));
494 		return false;
495 	}
496 
497 	ProcessFiles();
498 
499 	return true;
500 }
501 
EncodeUrl(const char * filename,char * url,int bufLen)502 void NzbFile::EncodeUrl(const char* filename, char* url, int bufLen)
503 {
504 	WString widefilename(filename);
505 
506 	char* end = url + bufLen;
507 	for (wchar_t* p = widefilename; *p && url < end - 3; p++)
508 	{
509 		wchar_t ch = *p;
510 		if (('0' <= ch && ch <= '9') ||
511 			('a' <= ch && ch <= 'z') ||
512 			('A' <= ch && ch <= 'Z') ||
513 			ch == '-' || ch == '.' || ch == '_' || ch == '~')
514 		{
515 			*url++ = (char)ch;
516 		}
517 		else
518 		{
519 			*url++ = '%';
520 			uint32 a = (uint32)ch >> 4;
521 			*url++ = a > 9 ? a - 10 + 'A' : a + '0';
522 			a = ch & 0xF;
523 			*url++ = a > 9 ? a - 10 + 'A' : a + '0';
524 		}
525 	}
526 	*url = '\0';
527 }
528 
ParseNzb(IUnknown * nzb)529 bool NzbFile::ParseNzb(IUnknown* nzb)
530 {
531 	MSXML::IXMLDOMDocumentPtr doc = nzb;
532 	MSXML::IXMLDOMNodePtr root = doc->documentElement;
533 
534 	MSXML::IXMLDOMNodePtr node = root->selectSingleNode("/nzb/head/meta[@type='password']");
535 	if (node)
536 	{
537 		_bstr_t password(node->Gettext());
538 		m_password = password;
539 	}
540 
541 	MSXML::IXMLDOMNodeListPtr fileList = root->selectNodes("/nzb/file");
542 	for (int i = 0; i < fileList->Getlength(); i++)
543 	{
544 		node = fileList->Getitem(i);
545 		MSXML::IXMLDOMNodePtr attribute = node->Getattributes()->getNamedItem("subject");
546 		if (!attribute) return false;
547 		_bstr_t subject(attribute->Gettext());
548 
549 		std::unique_ptr<FileInfo> fileInfo = std::make_unique<FileInfo>();
550 		fileInfo->SetSubject(subject);
551 
552 		attribute = node->Getattributes()->getNamedItem("date");
553 		if (attribute)
554 		{
555 			_bstr_t date(attribute->Gettext());
556 			fileInfo->SetTime(atoi(date));
557 		}
558 
559 		MSXML::IXMLDOMNodeListPtr groupList = node->selectNodes("groups/group");
560 		for (int g = 0; g < groupList->Getlength(); g++)
561 		{
562 			MSXML::IXMLDOMNodePtr node = groupList->Getitem(g);
563 			_bstr_t group = node->Gettext();
564 			fileInfo->GetGroups()->push_back((const char*)group);
565 		}
566 
567 		MSXML::IXMLDOMNodeListPtr segmentList = node->selectNodes("segments/segment");
568 		for (int g = 0; g < segmentList->Getlength(); g++)
569 		{
570 			MSXML::IXMLDOMNodePtr node = segmentList->Getitem(g);
571 			_bstr_t bid = node->Gettext();
572 			BString<1024> id("<%s>", (const char*)bid);
573 
574 			MSXML::IXMLDOMNodePtr attribute = node->Getattributes()->getNamedItem("number");
575 			if (!attribute) return false;
576 			_bstr_t number(attribute->Gettext());
577 
578 			attribute = node->Getattributes()->getNamedItem("bytes");
579 			if (!attribute) return false;
580 			_bstr_t bytes(attribute->Gettext());
581 
582 			int partNumber = atoi(number);
583 			int lsize = atoi(bytes);
584 
585 			if (partNumber > 0)
586 			{
587 				std::unique_ptr<ArticleInfo> article = std::make_unique<ArticleInfo>();
588 				article->SetPartNumber(partNumber);
589 				article->SetMessageId(id);
590 				article->SetSize(lsize);
591 				AddArticle(fileInfo.get(), std::move(article));
592 			}
593 		}
594 
595 		AddFileInfo(std::move(fileInfo));
596 	}
597 	return true;
598 }
599 
600 #else
601 
Parse()602 bool NzbFile::Parse()
603 {
604 #ifdef DISABLE_LIBXML2
605 	error("Could not parse rss feed, program was compiled without libxml2 support");
606 	return false;
607 #else
608 	xmlSAXHandler SAX_handler = {0};
609 	SAX_handler.startElement = reinterpret_cast<startElementSAXFunc>(SAX_StartElement);
610 	SAX_handler.endElement = reinterpret_cast<endElementSAXFunc>(SAX_EndElement);
611 	SAX_handler.characters = reinterpret_cast<charactersSAXFunc>(SAX_characters);
612 	SAX_handler.error = reinterpret_cast<errorSAXFunc>(SAX_error);
613 	SAX_handler.getEntity = reinterpret_cast<getEntitySAXFunc>(SAX_getEntity);
614 
615 	m_ignoreNextError = false;
616 
617 	int ret = xmlSAXUserParseFile(&SAX_handler, this, m_fileName);
618 
619 	if (ret != 0)
620 	{
621 		m_nzbInfo->AddMessage(Message::mkError, BString<1024>(
622 			"Error parsing nzb-file %s", FileSystem::BaseFileName(m_fileName)));
623 		return false;
624 	}
625 
626 	if (m_nzbInfo->GetFileList()->empty())
627 	{
628 		m_nzbInfo->AddMessage(Message::mkError, BString<1024>(
629 			"Error parsing nzb-file %s: file has no content", FileSystem::BaseFileName(m_fileName)));
630 		return false;
631 	}
632 
633 	ProcessFiles();
634 
635 	return true;
636 #endif
637 }
638 
Parse_StartElement(const char * name,const char ** atts)639 void NzbFile::Parse_StartElement(const char *name, const char **atts)
640 {
641 	BString<1024> tagAttrMessage("Malformed nzb-file, tag <%s> must have attributes", name);
642 
643 	m_tagContent.Clear();
644 
645 	if (!strcmp("file", name))
646 	{
647 		m_fileInfo = std::make_unique<FileInfo>();
648 		m_fileInfo->SetFilename(m_fileName);
649 
650 		if (!atts)
651 		{
652 			m_nzbInfo->AddMessage(Message::mkWarning, tagAttrMessage);
653 			return;
654 		}
655 
656 		for (int i = 0; atts[i]; i += 2)
657 		{
658 			const char* attrname = atts[i];
659 			const char* attrvalue = atts[i + 1];
660 			if (!strcmp("subject", attrname))
661 			{
662 				m_fileInfo->SetSubject(attrvalue);
663 			}
664 			if (!strcmp("date", attrname))
665 			{
666 				m_fileInfo->SetTime(atoi(attrvalue));
667 			}
668 		}
669 	}
670 	else if (!strcmp("segment", name))
671 	{
672 		if (!m_fileInfo)
673 		{
674 			m_nzbInfo->AddMessage(Message::mkWarning, "Malformed nzb-file, tag <segment> without tag <file>");
675 			return;
676 		}
677 
678 		if (!atts)
679 		{
680 			m_nzbInfo->AddMessage(Message::mkWarning, tagAttrMessage);
681 			return;
682 		}
683 
684 		int64 lsize = -1;
685 		int partNumber = -1;
686 
687 		for (int i = 0; atts[i]; i += 2)
688 		{
689 			const char* attrname = atts[i];
690 			const char* attrvalue = atts[i + 1];
691 			if (!strcmp("bytes", attrname))
692 			{
693 				lsize = atol(attrvalue);
694 			}
695 			if (!strcmp("number", attrname))
696 			{
697 				partNumber = atol(attrvalue);
698 			}
699 		}
700 
701 		if (partNumber > 0)
702 		{
703 			// new segment, add it!
704 			std::unique_ptr<ArticleInfo> article = std::make_unique<ArticleInfo>();
705 			article->SetPartNumber(partNumber);
706 			article->SetSize(lsize);
707 			m_article = article.get();
708 			AddArticle(m_fileInfo.get(), std::move(article));
709 		}
710 	}
711 	else if (!strcmp("meta", name))
712 	{
713 		if (!atts)
714 		{
715 			m_nzbInfo->AddMessage(Message::mkWarning, tagAttrMessage);
716 			return;
717 		}
718 		m_hasPassword = atts[0] && atts[1] && !strcmp("type", atts[0]) && !strcmp("password", atts[1]);
719 	}
720 }
721 
Parse_EndElement(const char * name)722 void NzbFile::Parse_EndElement(const char *name)
723 {
724 	if (!strcmp("file", name))
725 	{
726 		// Close the file element, add the new file to file-list
727 		AddFileInfo(std::move(m_fileInfo));
728 		m_article = nullptr;
729 	}
730 	else if (!strcmp("group", name))
731 	{
732 		if (!m_fileInfo)
733 		{
734 			// error: bad nzb-file
735 			return;
736 		}
737 
738 		m_fileInfo->GetGroups()->push_back(*m_tagContent);
739 		m_tagContent.Clear();
740 	}
741 	else if (!strcmp("segment", name))
742 	{
743 		if (!m_fileInfo || !m_article)
744 		{
745 			// error: bad nzb-file
746 			return;
747 		}
748 
749 		// Get the #text part
750 		BString<1024> id("<%s>", *m_tagContent);
751 		m_article->SetMessageId(id);
752 		m_article = nullptr;
753 	}
754 	else if (!strcmp("meta", name) && m_hasPassword)
755 	{
756 		m_password = m_tagContent;
757 	}
758 }
759 
Parse_Content(const char * buf,int len)760 void NzbFile::Parse_Content(const char *buf, int len)
761 {
762 	m_tagContent.Append(buf, len);
763 }
764 
SAX_StartElement(NzbFile * file,const char * name,const char ** atts)765 void NzbFile::SAX_StartElement(NzbFile* file, const char *name, const char **atts)
766 {
767 	file->Parse_StartElement(name, atts);
768 }
769 
SAX_EndElement(NzbFile * file,const char * name)770 void NzbFile::SAX_EndElement(NzbFile* file, const char *name)
771 {
772 	file->Parse_EndElement(name);
773 }
774 
SAX_characters(NzbFile * file,const char * xmlstr,int len)775 void NzbFile::SAX_characters(NzbFile* file, const char * xmlstr, int len)
776 {
777 	char* str = (char*)xmlstr;
778 
779 	// trim starting blanks
780 	int off = 0;
781 	for (int i = 0; i < len; i++)
782 	{
783 		char ch = str[i];
784 		if (ch == ' ' || ch == 10 || ch == 13 || ch == 9)
785 		{
786 			off++;
787 		}
788 		else
789 		{
790 			break;
791 		}
792 	}
793 
794 	int newlen = len - off;
795 
796 	// trim ending blanks
797 	for (int i = len - 1; i >= off; i--)
798 	{
799 		char ch = str[i];
800 		if (ch == ' ' || ch == 10 || ch == 13 || ch == 9)
801 		{
802 			newlen--;
803 		}
804 		else
805 		{
806 			break;
807 		}
808 	}
809 
810 	if (newlen > 0)
811 	{
812 		// interpret tag content
813 		file->Parse_Content(str + off, newlen);
814 	}
815 }
816 
SAX_getEntity(NzbFile * file,const char * name)817 void* NzbFile::SAX_getEntity(NzbFile* file, const char * name)
818 {
819 #ifdef DISABLE_LIBXML2
820 	void* e = nullptr;
821 #else
822 	xmlEntityPtr e = xmlGetPredefinedEntity((xmlChar* )name);
823 #endif
824 	if (!e)
825 	{
826 		file->m_nzbInfo->AddMessage(Message::mkWarning, "entity not found");
827 		file->m_ignoreNextError = true;
828 	}
829 
830 	return e;
831 }
832 
SAX_error(NzbFile * file,const char * msg,...)833 void NzbFile::SAX_error(NzbFile* file, const char *msg, ...)
834 {
835 	if (file->m_ignoreNextError)
836 	{
837 		file->m_ignoreNextError = false;
838 		return;
839 	}
840 
841 	va_list argp;
842 	va_start(argp, msg);
843 	char errMsg[1024];
844 	vsnprintf(errMsg, sizeof(errMsg), msg, argp);
845 	errMsg[1024-1] = '\0';
846 	va_end(argp);
847 
848 	// remove trailing CRLF
849 	for (char* pend = errMsg + strlen(errMsg) - 1; pend >= errMsg && (*pend == '\n' || *pend == '\r' || *pend == ' '); pend--) *pend = '\0';
850 
851 	file->m_nzbInfo->AddMessage(Message::mkError, BString<1024>("Error parsing nzb-file: %s", errMsg));
852 }
853 #endif
854