1 /*
2  * iso-codes.cpp
3  *
4  * Copyright (C) 2017 Mauro Carvalho Chehab <mchehab+samsung@kernel.org>
5  * Copyright (C) 2017 Pino Toscano <pino@kde.org>
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 
18 #include "log.h"
19 
20 #include <KLocalizedString>
21 #include <QFile>
22 #include <QLocale>
23 #include <QRegularExpression>
24 #include <QStandardPaths>
25 #include <QXmlStreamReader>
26 
27 #include "iso-codes.h"
28 
29 namespace IsoCodes
30 {
load(QHash<QString,QString> * code_3letters,QHash<QString,QString> * code_2letters,QString file,QString main_key,QString entry_key,QString code_3_key,QString code_2_key,QString name_key)31 	static void load(QHash<QString, QString> *code_3letters,
32 			 QHash<QString, QString> *code_2letters,
33 			 QString file,
34 			 QString main_key,
35 			 QString entry_key,
36 			 QString code_3_key,
37 			 QString code_2_key,
38 			 QString name_key)
39 	{
40 		if (!code_3letters->isEmpty())
41 			return;
42 
43 		const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, file);
44 		if (fileName.isEmpty()) {
45 			qCInfo(logConfig,
46 			       "Could not locate %s (is iso-codes installed?)",
47 			       qPrintable(file));
48 			return;
49 		}
50 
51 		QFile f(fileName);
52 		if (!f.open(QIODevice::ReadOnly)) {
53 			qCWarning(logConfig,
54 				  "Could not open %s (%s)",
55 				  qPrintable(fileName),
56 				  qPrintable(f.errorString()));
57 			return;
58 		}
59 
60 		QXmlStreamReader r(&f);
61 		bool inDoc = false;
62 		while (!r.atEnd()) {
63 			const QXmlStreamReader::TokenType t = r.readNext();
64 			QStringRef name;
65 			switch (t) {
66 			case QXmlStreamReader::StartElement:
67 				name = r.name();
68 				if (inDoc && name == entry_key) {
69 					const QXmlStreamAttributes attrs = r.attributes();
70 					const QString code3 = attrs.value(code_3_key).toString().toUpper();
71 					const QString lang = attrs.value(name_key).toString();
72 					code_3letters->insert(code3, lang);
73 					if (code_2letters) {
74 						const QString code2 = attrs.value(code_2_key).toString().toUpper();
75 						if (code2 != "")
76 							code_2letters->insert(code2, code3);
77 					}
78 				} else if (name == main_key) {
79 					inDoc = true;
80 				}
81 				break;
82 			case QXmlStreamReader::EndElement:
83 				name = r.name();
84 				if (inDoc && name == main_key) {
85 					inDoc = false;
86 				}
87 				break;
88 			case QXmlStreamReader::NoToken:
89 			case QXmlStreamReader::Invalid:
90 			case QXmlStreamReader::StartDocument:
91 			case QXmlStreamReader::EndDocument:
92 			case QXmlStreamReader::Characters:
93 			case QXmlStreamReader::Comment:
94 			case QXmlStreamReader::DTD:
95 			case QXmlStreamReader::EntityReference:
96 			case QXmlStreamReader::ProcessingInstruction:
97 				break;
98 			}
99 		}
100 		if (code_3letters->isEmpty())
101 			qCWarning(logConfig,
102 				  "Error parsing %s: no entries found.",
103 				  qPrintable(fileName));
104 	}
105 
106 	/*
107 	 * ISO 639-2 language codes
108 	 * Loaded and translated at runtime from iso-codes.
109 	 */
110 	static QHash<QString, QString> iso639_2_codes;
111 
112 	/*
113 	 * ISO 639-1 to ISO 639-2 language code conversion
114 	 * Loaded and translated at runtime from iso-codes.
115 	 */
116 	static QHash<QString, QString> iso639_1_codes;
117 
getLanguage(const QString & iso_code,QString * language)118 	bool getLanguage(const QString &iso_code, QString *language)
119 	{
120 		static bool first = true;
121 
122 		QString code = iso_code.toUpper();
123 
124 		if (code == "QAA") {
125 			*language = i18n("Original Language");
126 			return true;
127 		}
128 
129 		if (first) {
130 			load(&iso639_2_codes,
131 			     &iso639_1_codes,
132 			     QString("xml/iso-codes/iso_639-2.xml"),
133 			     QLatin1String("iso_639_entries"),
134 			     QLatin1String("iso_639_entry"),
135 			     QLatin1String("iso_639_2B_code"),
136 			     QLatin1String("iso_639_1_code"),
137 			     QLatin1String("name"));
138 			first = false;
139 		}
140 
141 		QHash<QString, QString>::ConstIterator it = iso639_2_codes.constFind(code);
142 		if (it == iso639_2_codes.constEnd()) {
143 			/*
144 			 * The ETSI EN 300 468 Annex F spec defines the
145 			 * original audio soundtrack code to be "QAA". Yet,
146 			 * TV bundle providers could use something else.
147 			 *
148 			 * At least here, my provider actually uses "ORG"
149 			 * instead. Currently, this is an unused ISO 639
150 			 * code, so we can safely accept it as well,
151 			 * at least as a fallback code while ISO doesn't
152 			 * define it.
153 			 */
154 			if (code == "ORG") {
155 				*language = i18n("Original Language");
156 				return true;
157 			}
158 			return false;
159 		}
160 
161 		if (language) {
162 			*language = i18nd("iso_639-2", it.value().toUtf8().constData());
163 		}
164 		return true;
165 	}
166 
code2Convert(const QString & code2)167 	const QString code2Convert(const QString &code2)
168 	{
169 		static bool first = true;
170 
171 		QString code = code2.toUpper();
172 
173 		/* Ignore any embedded Country data */
174 		code.remove(QRegularExpression("_.*"));
175 
176 		if (first) {
177 			load(&iso639_2_codes,
178 			     &iso639_1_codes,
179 			     QString("xml/iso-codes/iso_639-2.xml"),
180 			     QLatin1String("iso_639_entries"),
181 			     QLatin1String("iso_639_entry"),
182 			     QLatin1String("iso_639_2B_code"),
183 			     QLatin1String("iso_639_1_code"),
184 			     QLatin1String("name"));
185 			first = false;
186 		}
187 
188 		QHash<QString, QString>::ConstIterator it = iso639_1_codes.constFind(code);
189 		if (it == iso639_1_codes.constEnd()) {
190 			return "QAA";
191 		}
192 		return it.value().toUtf8().constData();
193 	}
194 
195 	/*
196 	* ISO 3166-1 Alpha 3 Country codes
197 	* Loaded and translated at runtime from iso-codes.
198 	*/
199 	static QHash<QString, QString> iso3166_1_codes, iso3166_2_codes;
200 
getCountry(const QString & _code,QString * country)201 	bool getCountry(const QString &_code, QString *country)
202 	{
203 		static bool first = true;
204 		QString code;
205 
206 		if (first) {
207 			load(&iso3166_1_codes,
208 			     &iso3166_2_codes,
209 			     QString("xml/iso-codes/iso_3166-1.xml"),
210 			     QLatin1String("iso_3166_entries"),
211 			     QLatin1String("iso_3166_entry"),
212 			     QLatin1String("alpha_3_code"),
213 			     QString("alpha_2_code"),
214 			     QLatin1String("name"));
215 			first = false;
216 		}
217 
218 		if (_code.size() == 2) {
219 			QHash<QString, QString>::ConstIterator it = iso3166_1_codes.constFind(code);
220 			if (it == iso3166_2_codes.constEnd())
221 				return false;
222 			code = it.value();
223 		} else {
224 			code = _code;
225 		}
226 
227 		QHash<QString, QString>::ConstIterator it = iso3166_1_codes.constFind(code);
228 		if (it == iso3166_1_codes.constEnd()) {
229 			return false;
230 		}
231 
232 		if (country) {
233 			*country = i18nd("iso_3166-1", it.value().toUtf8().constData());
234 		}
235 		return true;
236 	}
237 }
238