Skip to content

Commit f66e205

Browse files
authored
Merge pull request #283 from vslavik/devel/enclosure-selection-fixes
2 parents 181e9d2 + c36170d commit f66e205

File tree

5 files changed

+199
-135
lines changed

5 files changed

+199
-135
lines changed

src/appcast.cpp

+173-116
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,111 @@
2727
#include "error.h"
2828

2929
#include <expat.h>
30-
#include <vector>
3130
#include <algorithm>
31+
#include <iterator>
32+
#include <vector>
3233
#include <windows.h>
3334

3435
namespace winsparkle
3536
{
3637

38+
namespace
39+
{
40+
41+
// OS identification strings:
42+
43+
#define OS_MARKER_GENERIC "windows"
44+
#ifdef _WIN64
45+
#if defined(__AARCH64EL__) || defined(_M_ARM64)
46+
#define OS_MARKER_ARCH "windows-arm64"
47+
#else
48+
#define OS_MARKER_ARCH "windows-x64"
49+
#endif // defined(__AARCH64EL__) || defined(_M_ARM64)
50+
#else
51+
#define OS_MARKER_ARCH "windows-x86"
52+
#endif // _WIN64
53+
54+
55+
// Misc helper functions:
56+
57+
bool is_compatible_with_windows_version(const Appcast &item)
58+
{
59+
if (item.MinOSVersion.empty())
60+
return true;
61+
62+
OSVERSIONINFOEXW osvi = { sizeof(osvi), 0, 0, 0, 0, { 0 }, 0, 0 };
63+
DWORDLONG const dwlConditionMask = VerSetConditionMask(
64+
VerSetConditionMask(
65+
VerSetConditionMask(
66+
0, VER_MAJORVERSION, VER_GREATER_EQUAL),
67+
VER_MINORVERSION, VER_GREATER_EQUAL),
68+
VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);
69+
70+
sscanf(item.MinOSVersion.c_str(), "%lu.%lu.%hu", &osvi.dwMajorVersion,
71+
&osvi.dwMinorVersion, &osvi.wServicePackMajor);
72+
73+
return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION |
74+
VER_SERVICEPACKMAJOR, dwlConditionMask) != FALSE;
75+
}
76+
77+
78+
// Checks if the item is compatible with the running OS, that is:
79+
// - is not for a different OS
80+
// - is either for current architecture or is architecture-independent
81+
//
82+
// E.g. returns true if item is
83+
// - "windows-arm64" on 64bit ARM
84+
// - "windows-x64" on 64bit Intel/AMD
85+
// - "windows-x86" on 32bit
86+
// - "windows" on any Windows arch
87+
// - empty string on any OS
88+
inline bool is_compatible_with_os_arch(const Appcast::Enclosure& enclosure)
89+
{
90+
return enclosure.OS.empty() || enclosure.OS == OS_MARKER_GENERIC || enclosure.OS == OS_MARKER_ARCH;
91+
}
92+
93+
94+
// Finds the best enclosure for the current OS and architecture.
95+
Appcast::Enclosure find_best_enclosure_for_os_arch(const std::vector<Appcast::Enclosure>& enclosures)
96+
{
97+
// filter out incompatible enclosures:
98+
std::vector<Appcast::Enclosure> compatible;
99+
std::copy_if(enclosures.begin(), enclosures.end(), std::back_inserter(compatible), is_compatible_with_os_arch);
100+
if (compatible.empty())
101+
return Appcast::Enclosure();
102+
103+
// is there arch-specific enclosure?
104+
auto it = std::find_if(compatible.begin(), compatible.end(),
105+
[](const Appcast::Enclosure& e) { return e.OS == OS_MARKER_ARCH; });
106+
if (it != compatible.end())
107+
return *it;
108+
109+
// is there an enclosure explicitly marked as for windows?
110+
it = std::find_if(compatible.begin(), compatible.end(),
111+
[](const Appcast::Enclosure& e) { return e.OS == OS_MARKER_GENERIC; });
112+
if (it != compatible.end())
113+
return *it;
114+
115+
// because all enclosures are compatible, any one will do, e.g. the first one:
116+
return compatible.front();
117+
}
118+
119+
120+
void trim_whitespace(std::string& s)
121+
{
122+
size_t startpos = s.find_first_not_of(" \t\r\n");
123+
if (startpos != std::string::npos)
124+
s = s.substr(startpos);
125+
size_t endpos = s.find_last_not_of(" \t\r\n");
126+
if (endpos != std::string::npos)
127+
s = s.substr(0, endpos + 1);
128+
}
129+
130+
37131
/*--------------------------------------------------------------------------*
38132
XML parsing
39133
*--------------------------------------------------------------------------*/
40134

41-
namespace
42-
{
43-
44135
#define MVAL(x) x
45136
#define CONCAT3(a,b,c) MVAL(a)##MVAL(b)##MVAL(c)
46137

@@ -66,8 +157,7 @@ namespace
66157
#define NODE_VERSION ATTR_VERSION // These can be nodes or
67158
#define NODE_SHORTVERSION ATTR_SHORTVERSION // attributes.
68159
#define NODE_DSASIGNATURE ATTR_DSASIGNATURE
69-
#define OS_MARKER "windows"
70-
#define OS_MARKER_LEN 7
160+
71161

72162
// context data for the parser
73163
struct ContextData
@@ -78,38 +168,36 @@ struct ContextData
78168
in_version(0), in_shortversion(0), in_dsasignature(0), in_min_os_version(0)
79169
{}
80170

171+
// call when entering <item> element
172+
void reset_for_new_item()
173+
{
174+
current = Appcast();
175+
enclosures.clear();
176+
legacy_dsa_signature.clear();
177+
}
178+
81179
// the parser we're using
82180
XML_Parser& parser;
83181

84182
// is inside <channel>, <item> or <sparkle:releaseNotesLink>, <title>, <description>, or <link> respectively?
85183
int in_channel, in_item, in_relnotes, in_title, in_description, in_link;
86184

87-
// is inside <sparkle:version> or <sparkle:shortVersionString> node?
185+
// is inside <sparkle:version> or <sparkle:shortVersionString> etc. node?
88186
int in_version, in_shortversion, in_dsasignature, in_min_os_version;
89187

90-
// parsed <item>s
91-
std::vector<Appcast> items;
92-
};
93-
94-
bool is_windows_version_acceptable(const Appcast &item)
95-
{
96-
if (item.MinOSVersion.empty())
97-
return true;
188+
// currently parsed item
189+
Appcast current;
98190

99-
OSVERSIONINFOEXW osvi = { sizeof(osvi), 0, 0, 0, 0, { 0 }, 0, 0 };
100-
DWORDLONG const dwlConditionMask = VerSetConditionMask(
101-
VerSetConditionMask(
102-
VerSetConditionMask(
103-
0, VER_MAJORVERSION, VER_GREATER_EQUAL),
104-
VER_MINORVERSION, VER_GREATER_EQUAL),
105-
VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL);
191+
// enclosures encountered so far
192+
std::vector<Appcast::Enclosure> enclosures;
106193

107-
sscanf(item.MinOSVersion.c_str(), "%lu.%lu.%hu", &osvi.dwMajorVersion,
108-
&osvi.dwMinorVersion, &osvi.wServicePackMajor);
194+
// signature present as <sparkle:dsaSignature>, not enclosure attribute
195+
std::string legacy_dsa_signature;
196+
197+
// parsed <item>s
198+
std::vector<Appcast> all_items;
199+
};
109200

110-
return VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION |
111-
VER_SERVICEPACKMAJOR, dwlConditionMask) != FALSE;
112-
}
113201

114202
void XMLCALL OnStartElement(void *data, const char *name, const char **attrs)
115203
{
@@ -122,8 +210,7 @@ void XMLCALL OnStartElement(void *data, const char *name, const char **attrs)
122210
else if ( ctxt.in_channel && strcmp(name, NODE_ITEM) == 0 )
123211
{
124212
ctxt.in_item++;
125-
Appcast item;
126-
ctxt.items.push_back(item);
213+
ctxt.reset_for_new_item();
127214
}
128215
else if ( ctxt.in_item )
129216
{
@@ -161,80 +248,43 @@ void XMLCALL OnStartElement(void *data, const char *name, const char **attrs)
161248
}
162249
else if (strcmp(name, NODE_ENCLOSURE) == 0)
163250
{
164-
if (!ctxt.items.empty())
251+
Appcast& item = ctxt.current;
252+
Appcast::Enclosure enclosure;
253+
254+
for (int i = 0; attrs[i]; i += 2)
165255
{
166-
Appcast& item = ctxt.items.back();
167-
for (int i = 0; attrs[i]; i += 2)
168-
{
169-
const char* name = attrs[i];
170-
const char* value = attrs[i + 1];
171-
172-
if (strcmp(name, ATTR_URL) == 0)
173-
item.DownloadURL = value;
174-
else if (strcmp(name, ATTR_VERSION) == 0)
175-
item.Version = value;
176-
else if (strcmp(name, ATTR_SHORTVERSION) == 0)
177-
item.ShortVersionString = value;
178-
else if (strcmp(name, ATTR_DSASIGNATURE) == 0)
179-
item.DsaSignature = value;
180-
else if (strcmp(name, ATTR_OS) == 0)
181-
item.Os = value;
182-
else if (strcmp(name, ATTR_ARGUMENTS) == 0)
183-
item.InstallerArguments = value;
184-
}
256+
const char* name = attrs[i];
257+
const char* value = attrs[i + 1];
258+
259+
if (strcmp(name, ATTR_URL) == 0)
260+
enclosure.DownloadURL = value;
261+
else if (strcmp(name, ATTR_DSASIGNATURE) == 0)
262+
enclosure.DsaSignature = value;
263+
else if (strcmp(name, ATTR_OS) == 0)
264+
enclosure.OS = value;
265+
else if (strcmp(name, ATTR_ARGUMENTS) == 0)
266+
enclosure.InstallerArguments = value;
267+
268+
// legacy syntax where version info was on enclosure, not item:
269+
else if (strcmp(name, ATTR_VERSION) == 0)
270+
item.Version = value;
271+
else if (strcmp(name, ATTR_SHORTVERSION) == 0)
272+
item.ShortVersionString = value;
185273
}
274+
275+
// note: we intentionally include incompatible enclosures in the list so that
276+
// we can check for that case later in OnEndElement() and skip the entire <item>
277+
if (enclosure.IsValid())
278+
ctxt.enclosures.push_back(enclosure);
186279
}
187280
else if (strcmp(name, NODE_CRITICAL_UPDATE) == 0)
188281
{
189-
if (!ctxt.items.empty())
190-
ctxt.items.back().CriticalUpdate = true;
282+
ctxt.current.CriticalUpdate = true;
191283
}
192284
}
193285
}
194286

195287

196-
/**
197-
* Returns true if item os is exactly "windows"
198-
* or if item is "windows-arm64" on 64bit ARM
199-
* or if item is "windows-x64" on 64bit Intel/AMD
200-
* or if item is "windows-x86" on 32bit
201-
* and is above minimum version
202-
*/
203-
bool is_suitable_windows_item(const Appcast &item)
204-
{
205-
if (!is_windows_version_acceptable(item))
206-
return false;
207-
208-
if (item.Os == OS_MARKER)
209-
return true;
210-
211-
if (item.Os.compare(0, OS_MARKER_LEN, OS_MARKER) != 0)
212-
return false;
213-
214-
// Check suffix for matching bitness
215-
#ifdef _WIN64
216-
#if defined(__AARCH64EL__) || defined(_M_ARM64)
217-
return item.Os.compare(OS_MARKER_LEN, std::string::npos, "-arm64") == 0;
218-
#else
219-
return item.Os.compare(OS_MARKER_LEN, std::string::npos, "-x64") == 0;
220-
#endif // defined(__AARCH64EL__) || defined(_M_ARM64)
221-
#else
222-
return item.Os.compare(OS_MARKER_LEN, std::string::npos, "-x86") == 0;
223-
#endif // _WIN64
224-
}
225-
226-
227-
void trim_whitespace(std::string& s)
228-
{
229-
size_t startpos = s.find_first_not_of(" \t\r\n");
230-
if (startpos != std::string::npos)
231-
s = s.substr(startpos);
232-
size_t endpos = s.find_last_not_of(" \t\r\n");
233-
if (endpos != std::string::npos)
234-
s = s.substr(0, endpos + 1);
235-
}
236-
237-
238288
void XMLCALL OnEndElement(void *data, const char *name)
239289
{
240290
ContextData& ctxt = *static_cast<ContextData*>(data);
@@ -276,8 +326,31 @@ void XMLCALL OnEndElement(void *data, const char *name)
276326
else if (strcmp(name, NODE_ITEM) == 0)
277327
{
278328
ctxt.in_item--;
279-
if (is_suitable_windows_item(ctxt.items[ctxt.items.size() - 1]))
329+
330+
Appcast& item = ctxt.current;
331+
332+
if (!ctxt.legacy_dsa_signature.empty() && item.enclosure.DsaSignature.empty())
333+
item.enclosure.DsaSignature = ctxt.legacy_dsa_signature;
334+
335+
if (!ctxt.enclosures.empty())
336+
{
337+
item.enclosure = find_best_enclosure_for_os_arch(ctxt.enclosures);
338+
if (!item.enclosure.IsValid())
339+
{
340+
// There are enclosures (e.g. weblink is not used), but all enclosures are
341+
// incompatible. This means the <item> is not meant for this OS and should be
342+
// skipped (as Sparkle does; there may be another <item> for us).
343+
return;
344+
}
345+
}
346+
347+
if (item.IsValid() && is_compatible_with_windows_version(item))
348+
{
349+
ctxt.all_items.push_back(item);
350+
351+
// FIXME: this is premature, we should sort appcast items by date and pick the newest (as Sparkle does)
280352
XML_StopParser(ctxt.parser, XML_TRUE);
353+
}
281354
}
282355
}
283356
else if (strcmp(name, NODE_CHANNEL) == 0 )
@@ -293,10 +366,7 @@ void XMLCALL OnEndElement(void *data, const char *name)
293366
void XMLCALL OnText(void *data, const char *s, int len)
294367
{
295368
ContextData& ctxt = *static_cast<ContextData*>(data);
296-
if (ctxt.items.empty())
297-
return;
298-
299-
Appcast& item = ctxt.items.back();
369+
Appcast& item = ctxt.current;
300370

301371
if (ctxt.in_relnotes)
302372
{
@@ -326,7 +396,7 @@ void XMLCALL OnText(void *data, const char *s, int len)
326396
}
327397
else if (ctxt.in_dsasignature)
328398
{
329-
item.DsaSignature.append(s, len);
399+
ctxt.legacy_dsa_signature.assign(s, len);
330400
}
331401
else if (ctxt.in_min_os_version)
332402
{
@@ -365,25 +435,12 @@ Appcast Appcast::Load(const std::string& xml)
365435

366436
XML_ParserFree(p);
367437

368-
if (ctxt.items.empty())
438+
if (ctxt.all_items.empty())
369439
return Appcast(); // invalid
370440

371-
/*
372-
* Search for first <item> which specifies with the attribute sparkle:os set to "windows"
373-
* or "windows-x64"/"windows-arm64"/"windows-x86" based on this modules bitness and meets the minimum
374-
* os version, if set. If none, use the first item that meets the minimum os version, if set.
375-
*/
376-
std::vector<Appcast>::iterator it = std::find_if(ctxt.items.begin(), ctxt.items.end(), is_suitable_windows_item);
377-
if (it != ctxt.items.end())
378-
return *it;
379-
else
380-
{
381-
it = std::find_if(ctxt.items.begin(), ctxt.items.end(), is_windows_version_acceptable);
382-
if (it != ctxt.items.end())
383-
return *it;
384-
else
385-
return Appcast(); // There are no items that meet the set minimum os version
386-
}
441+
// the items were already filtered to only include those compatible with the current OS + arch
442+
// and meeting minimum OS version requirements, so we can just return the first one
443+
return ctxt.all_items.front();
387444
}
388445

389446
} // namespace winsparkle

0 commit comments

Comments
 (0)