1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
5  * Copyright (c) 2009-2016, The OpenClonk Team and contributors
6  *
7  * Distributed under the terms of the ISC license; see accompanying file
8  * "COPYING" for details.
9  *
10  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11  * See accompanying file "TRADEMARK" for details.
12  *
13  * To redistribute this file separately, substitute the full license texts
14  * for the above references.
15  */
16 
17 /*
18    Language module
19    - handles external language packs
20    - provides info on selectable languages by scanning string tables
21    - loads and sets a language string table (ResStrTable) based on a specified language sequence
22 
23 */
24 
25 #include "C4Include.h"
26 #include "c4group/C4Language.h"
27 
28 #include "game/C4Application.h"
29 #include "c4group/C4Components.h"
30 
31 template<size_t iBufferSize>
GetRelativePath(const char * strPath,const char * strRelativeTo,char (& strBuffer)[iBufferSize])32 static bool GetRelativePath(const char *strPath, const char *strRelativeTo, char(&strBuffer)[iBufferSize])
33 {
34 	// Specified path is relative to base path
35 	// Copy relative section
36 	const char *szCpy;
37 	SCopy(szCpy = GetRelativePathS(strPath, strRelativeTo), strBuffer, iBufferSize);
38 	// return whether it was made relative
39 	return szCpy != strPath;
40 }
41 
42 C4Language Languages;
43 
C4Language()44 C4Language::C4Language()
45 {
46 	Infos = nullptr;
47 	PackGroupLocation[0] = 0;
48 }
49 
~C4Language()50 C4Language::~C4Language()
51 {
52 	Clear();
53 }
54 
Init()55 bool C4Language::Init()
56 {
57 	// Clear (to allow clean re-init)
58 	Clear();
59 
60 	// Make sure Language.ocg is unpacked (TODO: This won't work properly if Language.ocg is in system data path)
61 	// Assume for now that Language.ocg is either at a writable location or unpacked already.
62 	// TODO: Use all Language.c4gs that we find, and merge them.
63 	// TODO: Use gettext instead?
64 	StdStrBuf langPath;
65 	C4Reloc::iterator iter;
66 	for(iter = Reloc.begin(); iter != Reloc.end(); ++iter)
67 	{
68 		langPath.Copy((*iter).strBuf + DirSep + C4CFN_Languages);
69 		if(ItemExists(langPath.getData()))
70 		{
71 			if(DirectoryExists(langPath.getData()))
72 				break;
73 			if(C4Group_UnpackDirectory(langPath.getData()))
74 				break;
75 		}
76 	}
77 
78 	// Break if no language.ocg found
79 	if(iter != Reloc.end())
80 	{
81 		// Look for available language packs in Language.ocg
82 		C4Group *pPack;
83 		char strPackFilename[_MAX_FNAME + 1], strEntry[_MAX_FNAME + 1];
84 		if (PackDirectory.Open(langPath.getData()))
85 		{
86 			while (PackDirectory.FindNextEntry("*.ocg", strEntry))
87 			{
88 				sprintf(strPackFilename, "%s" DirSep "%s", C4CFN_Languages, strEntry);
89 				pPack = new C4Group();
90 				if (pPack->Open(strPackFilename))
91 				{
92 					Packs.RegisterGroup(*pPack, true, C4GSCnt_Language, false);
93 				}
94 				else
95 				{
96 					delete pPack;
97 				}
98 			}
99 		}
100 
101 		// Now create a pack group for each language pack (these pack groups are child groups
102 		// that browse along each pack to access requested data)
103 		for (int iPack = 0; (pPack = Packs.GetGroup(iPack)); iPack++)
104 			PackGroups.RegisterGroup(*(new C4Group), true, C4GSPrio_Base, C4GSCnt_Language);
105 	}
106 
107 	// Load language infos by scanning string tables (the engine doesn't really need this at the moment)
108 	InitInfos();
109 
110 	// Done
111 	return true;
112 }
113 
Clear()114 void C4Language::Clear()
115 {
116 	// Clear pack groups
117 	PackGroups.Clear();
118 	// Clear packs
119 	Packs.Clear();
120 	// Close pack directory
121 	PackDirectory.Close();
122 	// Clear infos
123 	C4LanguageInfo* pNext;
124 	while (Infos)
125 	{
126 		pNext = Infos->Next;
127 		delete Infos;
128 		Infos = pNext;
129 	}
130 	Infos = nullptr;
131 }
132 
GetPackCount()133 int C4Language::GetPackCount()
134 {
135 	return Packs.GetGroupCount();
136 }
137 
GetInfoCount()138 int C4Language::GetInfoCount()
139 {
140 	int iCount = 0;
141 	for (C4LanguageInfo *pInfo = Infos; pInfo; pInfo = pInfo->Next)
142 		iCount++;
143 	return iCount;
144 }
145 
146 // Returns a set of groups at the specified relative path within all open language packs.
147 
GetPackGroups(C4Group & hGroup)148 C4GroupSet C4Language::GetPackGroups(C4Group & hGroup)
149 {
150 	// Build a group set containing the provided group and
151 	// alternative groups for cross-loading from a language pack
152 	char strRelativePath[_MAX_PATH + 1];
153 	char strTargetLocation[_MAX_PATH + 1];
154 	char strPackPath[_MAX_PATH + 1];
155 	char strPackGroupLocation[_MAX_PATH + 1];
156 	char strAdvance[_MAX_PATH + 1];
157 
158 	// Store wanted target location
159 	SCopy(Config.AtRelativePath(hGroup.GetFullName().getData()), strRelativePath, _MAX_PATH);
160 	SCopy(strRelativePath, strTargetLocation, _MAX_PATH);
161 
162 	// Adjust location by scenario origin
163 	if (Game.C4S.Head.Origin.getLength() && SEqualNoCase(GetExtension(Game.C4S.Head.Origin.getData()), "ocs"))
164 	{
165 		const char *szScenarioRelativePath = GetRelativePathS(strRelativePath, Config.AtRelativePath(Game.ScenarioFilename));
166 		if (szScenarioRelativePath != strRelativePath)
167 		{
168 			// this is a path within the scenario! Change to origin.
169 			size_t iRestPathLen = SLen(szScenarioRelativePath);
170 			if (Game.C4S.Head.Origin.getLength() + 1 + iRestPathLen <= _MAX_PATH)
171 			{
172 				SCopy(Game.C4S.Head.Origin.getData(), strTargetLocation);
173 				if (iRestPathLen)
174 				{
175 					SAppendChar(DirectorySeparator, strTargetLocation);
176 					SAppend(szScenarioRelativePath, strTargetLocation);
177 				}
178 			}
179 		}
180 	}
181 
182 	// Process all language packs (and their respective pack groups)
183 	C4Group *pPack, *pPackGroup;
184 	for (int iPack = 0; (pPack = Packs.GetGroup(iPack)) && (pPackGroup = PackGroups.GetGroup(iPack)); iPack++)
185 	{
186 		// Get current pack group position within pack
187 		SCopy(pPack->GetFullName().getData(), strPackPath, _MAX_PATH);
188 		GetRelativePath(pPackGroup->GetFullName().getData(), strPackPath, strPackGroupLocation);
189 
190 		// Pack group is at correct position within pack: continue with next pack
191 		if (SEqualNoCase(strPackGroupLocation, strTargetLocation))
192 			continue;
193 
194 		// Try to backtrack until we can reach the target location as a relative child
195 		while ( strPackGroupLocation[0]
196 		        && !GetRelativePath(strTargetLocation, strPackGroupLocation, strAdvance)
197 		        && pPackGroup->OpenMother() )
198 		{
199 			// Update pack group location
200 			GetRelativePath(pPackGroup->GetFullName().getData(), strPackPath, strPackGroupLocation);
201 		}
202 
203 		// We can reach the target location as a relative child
204 		if (strPackGroupLocation[0] && GetRelativePath(strTargetLocation, strPackGroupLocation, strAdvance))
205 		{
206 			// Advance pack group to relative child
207 			pPackGroup->OpenChild(strAdvance);
208 		}
209 
210 		// Cannot reach by advancing: need to close and reopen (rewinding group file)
211 		else
212 		{
213 			// Close pack group (if it is open at all)
214 			pPackGroup->Close();
215 			// Reopen pack group to relative position in language pack if possible
216 			pPackGroup->OpenAsChild(pPack, strTargetLocation);
217 		}
218 
219 	}
220 
221 	// Store new target location
222 	SCopy(strTargetLocation, PackGroupLocation, _MAX_FNAME);
223 
224 	C4GroupSet r;
225 	// Provided group gets highest priority
226 	r.RegisterGroup(hGroup, false, 1000, C4GSCnt_Component);
227 	// register currently open pack groups
228 	r.RegisterGroups(PackGroups, C4GSCnt_Language);
229 	return r;
230 }
231 
LoadComponentHost(C4ComponentHost * host,C4Group & hGroup,const char * szFilename,const char * szLanguage)232 bool C4Language::LoadComponentHost(C4ComponentHost *host, C4Group &hGroup, const char *szFilename, const char *szLanguage)
233 {
234 	assert(host);
235 	if (!host) return false;
236 	C4GroupSet hGroups = ::Languages.GetPackGroups(hGroup);
237 	return host->Load(hGroups, szFilename, szLanguage);
238 }
239 
InitInfos()240 void C4Language::InitInfos()
241 {
242 	C4Group hGroup;
243 	// First, look in System.ocg
244 	if (Reloc.Open(hGroup, C4CFN_System))
245 	{
246 		LoadInfos(hGroup);
247 		hGroup.Close();
248 	}
249 	// Now look through the registered packs
250 	C4Group *pPack;
251 	for (int iPack = 0; (pPack = Packs.GetGroup(iPack)); iPack++)
252 		// Does it contain a System.ocg child group?
253 		if (hGroup.OpenAsChild(pPack, C4CFN_System))
254 		{
255 			LoadInfos(hGroup);
256 			hGroup.Close();
257 		}
258 }
259 
260 namespace
261 {
GetResStr(const char * id,const char * stringtbl)262 	std::string GetResStr(const char *id, const char *stringtbl)
263 	{
264 		// The C++11 standard does not specify whether $ and ^ match
265 		// the beginning or end of a line, respectively, and it seems
266 		// like in some implementations they only match the beginning
267 		// or end of the whole string. See also #1127.
268 		static std::regex line_pattern("(?:\n|^)([^=]+)=(.*?)\r?(?=\n|$)", static_cast<std::regex::flag_type>(std::regex_constants::optimize | std::regex_constants::ECMAScript));
269 
270 		assert(stringtbl);
271 		if (!stringtbl)
272 		{
273 			return std::string();
274 		}
275 
276 		// Get beginning and end iterators of stringtbl
277 		const char *begin = stringtbl;
278 		const char *end = begin + std::char_traits<char>::length(begin);
279 
280 		for (auto it = std::cregex_iterator(begin, end, line_pattern); it != std::cregex_iterator(); ++it)
281 		{
282 			assert(it->size() == 3);
283 			if (it->size() != 3)
284 				continue;
285 
286 			std::string key = (*it)[1];
287 			if (key != id)
288 				continue;
289 
290 			std::string val = (*it)[2];
291 			return val;
292 		}
293 
294 		// If we get here, there was no such string in the string table
295 		// return the input string so there's at least *something*
296 		return id;
297 	}
298 
299 	template<size_t N>
CopyResStr(const char * id,const char * stringtbl,char (& dest)[N])300 	void CopyResStr(const char *id, const char *stringtbl, char (&dest)[N])
301 	{
302 		std::string value = GetResStr(id, stringtbl);
303 		std::strncpy(dest, value.c_str(), N);
304 		dest[N - 1] = '\0';
305 	}
306 }
307 
LoadInfos(C4Group & hGroup)308 void C4Language::LoadInfos(C4Group &hGroup)
309 {
310 	char strEntry[_MAX_FNAME + 1];
311 	char *strTable;
312 	// Look for language string tables
313 	hGroup.ResetSearch();
314 	while (hGroup.FindNextEntry(C4CFN_Language, strEntry))
315 		// For now, we will only load info on the first string table found for a given
316 		// language code as there is currently no handling for selecting different string tables
317 		// of the same code - the system always loads the first string table found for a given code
318 		if (!FindInfo(GetFilenameOnly(strEntry) + SLen(GetFilenameOnly(strEntry)) - 2))
319 			// Load language string table
320 			if (hGroup.LoadEntry(strEntry, &strTable, nullptr, 1))
321 			{
322 				// New language info
323 				C4LanguageInfo *pInfo = new C4LanguageInfo;
324 				// Get language code by entry name
325 				SCopy(GetFilenameOnly(strEntry) + SLen(GetFilenameOnly(strEntry)) - 2, pInfo->Code, 2);
326 				SCapitalize(pInfo->Code);
327 				// Get language name, info, fallback from table
328 				CopyResStr("IDS_LANG_NAME", strTable, pInfo->Name);
329 				CopyResStr("IDS_LANG_INFO", strTable, pInfo->Info);
330 				CopyResStr("IDS_LANG_FALLBACK", strTable, pInfo->Fallback);
331 				// Safety: pipe character is not allowed in any language info string
332 				SReplaceChar(pInfo->Name, '|', ' ');
333 				SReplaceChar(pInfo->Info, '|', ' ');
334 				SReplaceChar(pInfo->Fallback, '|', ' ');
335 				// Delete table
336 				delete [] strTable;
337 				// Add info to list
338 				pInfo->Next = Infos;
339 				Infos = pInfo;
340 			}
341 }
342 
GetInfo(int iIndex)343 C4LanguageInfo* C4Language::GetInfo(int iIndex)
344 {
345 	for (C4LanguageInfo *pInfo = Infos; pInfo; pInfo = pInfo->Next)
346 		if (iIndex <= 0)
347 			return pInfo;
348 		else
349 			iIndex--;
350 	return nullptr;
351 }
352 
FindInfo(const char * strCode)353 C4LanguageInfo* C4Language::FindInfo(const char *strCode)
354 {
355 	for (C4LanguageInfo *pInfo = Infos; pInfo; pInfo = pInfo->Next)
356 		if (SEqualNoCase(pInfo->Code, strCode, 2))
357 			return pInfo;
358 	return nullptr;
359 }
360 
LoadLanguage(const char * strLanguages)361 bool C4Language::LoadLanguage(const char *strLanguages)
362 {
363 	// Clear old string table
364 	ClearLanguage();
365 	// Try to load string table according to language sequence
366 	char strLanguageCode[2 + 1];
367 	for (int i = 0; SCopySegment(strLanguages, i, strLanguageCode, ',', 2, true); i++)
368 		if (InitStringTable(strLanguageCode))
369 			return true;
370 	// No matching string table found: hardcoded fallback to US
371 	if (InitStringTable("US"))
372 		return true;
373 	// No string table present: this is really bad
374 	Log("Error loading language string table.");
375 	return false;
376 }
377 
InitStringTable(const char * strCode)378 bool C4Language::InitStringTable(const char *strCode)
379 {
380 	C4Group hGroup;
381 	// First, look in System.ocg
382 	if (LoadStringTable(Application.SystemGroup, strCode))
383 		return true;
384 	// Now look through the registered packs
385 	C4Group *pPack;
386 	for (int iPack = 0; (pPack = Packs.GetGroup(iPack)); iPack++)
387 		// Does it contain a System.ocg child group?
388 		if (hGroup.OpenAsChild(pPack, C4CFN_System))
389 		{
390 			if (LoadStringTable(hGroup, strCode))
391 				{ hGroup.Close(); return true; }
392 			hGroup.Close();
393 		}
394 	// No matching string table found
395 	return false;
396 }
397 
LoadStringTable(C4Group & hGroup,const char * strCode)398 bool C4Language::LoadStringTable(C4Group &hGroup, const char *strCode)
399 {
400 	// Compose entry name
401 	char strEntry[_MAX_FNAME + 1];
402 	sprintf(strEntry, "Language%s.txt", strCode); // ...should use C4CFN_Language here
403 	// Load string table
404 	if (!C4LangStringTable::GetSystemStringTable().Load(hGroup, strEntry))
405 		return false;
406 	// Success
407 	return true;
408 }
409 
ClearLanguage()410 void C4Language::ClearLanguage()
411 {
412 	// Clear resource string table
413 	C4LangStringTable::GetSystemStringTable().Clear();
414 }
415 
416 // Closes any open language pack that has the specified path.
417 
CloseGroup(const char * strPath)418 bool C4Language::CloseGroup(const char *strPath)
419 {
420 	// Check all open language packs
421 	C4Group *pPack;
422 	for (int iPack = 0; (pPack = Packs.GetGroup(iPack)); iPack++)
423 		if (ItemIdentical(strPath, pPack->GetFullName().getData()))
424 		{
425 			Packs.UnregisterGroup(iPack);
426 			return true;
427 		}
428 	// No pack of that path
429 	return false;
430 }
431