OLD | NEW |
| (Empty) |
1 // Copyright 2011 Google Inc. | |
2 // | |
3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 // you may not use this file except in compliance with the License. | |
5 // You may obtain a copy of the License at | |
6 // | |
7 // http://www.apache.org/licenses/LICENSE-2.0 | |
8 // | |
9 // Unless required by applicable law or agreed to in writing, software | |
10 // distributed under the License is distributed on an "AS IS" BASIS, | |
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 // See the License for the specific language governing permissions and | |
13 // limitations under the License. | |
14 // ======================================================================== | |
15 | |
16 // Utility class to manage a set of experiment labels. Experiment labels | |
17 // are a set of key/value pairs used to track an install's "membership" in | |
18 // A/B experiment groups issued by the Omaha server. Keys are strings with | |
19 // a limited character set (Perl \w, plus a few characters), while values | |
20 // consist of a string and an expiration date. Label sets are stored per-app | |
21 // and transmitted to the server as part of requests (pings / update checks), | |
22 // and a delta is potentially returned from the server with each response. | |
23 // | |
24 // Experiment labels are serialized with key/value separated by an equals, and | |
25 // value/expiration by a pipe symbol; the expiration is represented in RFC822 | |
26 // format. For example: "test_key=test_value|Fri, 14 Aug 2015 16:13:03 GMT". | |
27 // If an app participates in multiple experiments simultaneously, labels are | |
28 // concatenated with semicolon delimiters. | |
29 | |
30 #include "omaha/common/experiment_labels.h" | |
31 | |
32 #include "omaha/base/debug.h" | |
33 #include "omaha/base/safe_format.h" | |
34 #include "omaha/common/app_registry_utils.h" | |
35 | |
36 namespace omaha { | |
37 | |
38 ExperimentLabels::ExperimentLabels() : labels_(), preserve_expired_(false) {} | |
39 | |
40 ExperimentLabels::~ExperimentLabels() {} | |
41 | |
42 int ExperimentLabels::NumLabels() const { | |
43 return labels_.size(); | |
44 } | |
45 | |
46 bool ExperimentLabels::ContainsKey(const CString& key) const { | |
47 ASSERT1(!key.IsEmpty()); | |
48 return labels_.find(key) != labels_.end(); | |
49 } | |
50 | |
51 void ExperimentLabels::GetLabelByIndex(int index, CString* key, CString* value, | |
52 time64* expiration) const { | |
53 ASSERT1(index >= 0); | |
54 ASSERT1(static_cast<LabelMap::size_type>(index) < labels_.size()); | |
55 | |
56 LabelMap::const_iterator cit = labels_.begin(); | |
57 std::advance(cit, static_cast<LabelMap::size_type>(index)); | |
58 if (key) { | |
59 *key = cit->first; | |
60 } | |
61 if (value) { | |
62 *value = cit->second.first; | |
63 } | |
64 if (expiration) { | |
65 *expiration = cit->second.second; | |
66 } | |
67 } | |
68 | |
69 bool ExperimentLabels::FindLabelByKey(const CString& key, CString* value, | |
70 time64* expiration) const { | |
71 ASSERT1(!key.IsEmpty()); | |
72 LabelMap::const_iterator cit = labels_.find(key); | |
73 if (labels_.end() == cit) { | |
74 return false; | |
75 } | |
76 | |
77 if (value) { | |
78 *value = cit->second.first; | |
79 } | |
80 if (expiration) { | |
81 *expiration = cit->second.second; | |
82 } | |
83 return true; | |
84 } | |
85 | |
86 bool ExperimentLabels::SetLabel(const CString& key, const CString& value, | |
87 time64 expiration) { | |
88 if (!IsLabelContentValid(key) || !IsLabelContentValid(value)) { | |
89 return false; | |
90 } | |
91 if (expiration < GetCurrent100NSTime() && !preserve_expired_) { | |
92 return false; | |
93 } | |
94 labels_[key] = std::make_pair(value, expiration); | |
95 return true; | |
96 } | |
97 | |
98 bool ExperimentLabels::ClearLabel(const CString& key) { | |
99 LabelMap::iterator it = labels_.find(key); | |
100 if (labels_.end() == it) { | |
101 return false; | |
102 } | |
103 labels_.erase(it); | |
104 return true; | |
105 } | |
106 | |
107 void ExperimentLabels::ExpireLabels() { | |
108 time64 current_time = GetCurrent100NSTime(); | |
109 for (LabelMap::iterator it = labels_.begin(); it != labels_.end(); ++it) { | |
110 if (it->second.second < current_time) { | |
111 it = labels_.erase(it); | |
112 if (it == labels_.end()) { | |
113 break; | |
114 } | |
115 } | |
116 } | |
117 } | |
118 | |
119 void ExperimentLabels::ClearAllLabels() { | |
120 labels_.clear(); | |
121 } | |
122 | |
123 CString ExperimentLabels::Serialize() const { | |
124 CString serialized; | |
125 time64 current_time = GetCurrent100NSTime(); | |
126 for (LabelMap::const_iterator cit = labels_.begin(); | |
127 cit != labels_.end(); | |
128 ++cit) { | |
129 if (preserve_expired_ || cit->second.second >= current_time) { | |
130 if (!serialized.IsEmpty()) { | |
131 serialized.Append(L";"); | |
132 } | |
133 FILETIME ft = {}; | |
134 Time64ToFileTime(cit->second.second, &ft); | |
135 SafeCStringAppendFormat(&serialized, L"%s=%s|%s", | |
136 cit->first, | |
137 cit->second.first, | |
138 ConvertTimeToGMTString(&ft)); | |
139 } | |
140 } | |
141 return serialized; | |
142 } | |
143 | |
144 bool ExperimentLabels::Deserialize(const CString& label_list) { | |
145 LabelMap new_labels; | |
146 if (DoDeserialize(&new_labels, label_list, preserve_expired_)) { | |
147 std::swap(labels_, new_labels); | |
148 return true; | |
149 } | |
150 return false; | |
151 } | |
152 | |
153 bool ExperimentLabels::DeserializeAndApplyDelta(const CString& label_list) { | |
154 LabelMap merged_labels = labels_; | |
155 if (DoDeserialize(&merged_labels, label_list, false)) { | |
156 std::swap(labels_, merged_labels); | |
157 return true; | |
158 } | |
159 return false; | |
160 } | |
161 | |
162 void ExperimentLabels::SetPreserveExpiredLabels(bool preserve) { | |
163 preserve_expired_ = preserve; | |
164 } | |
165 | |
166 HRESULT ExperimentLabels::WriteToRegistry(bool is_machine, | |
167 const CString& app_id) { | |
168 return app_registry_utils::SetExperimentLabels(is_machine, app_id, | |
169 Serialize()); | |
170 } | |
171 | |
172 HRESULT ExperimentLabels::ReadFromRegistry(bool is_machine, | |
173 const CString& app_id) { | |
174 CString label_list; | |
175 HRESULT hr = app_registry_utils::GetExperimentLabels(is_machine, app_id, | |
176 &label_list); | |
177 if (FAILED(hr)) { | |
178 return hr; | |
179 } | |
180 return Deserialize(label_list) ? S_OK : E_FAIL; | |
181 } | |
182 | |
183 bool ExperimentLabels::IsStringValidLabelSet(const CString& label_list) { | |
184 ExperimentLabels labels; | |
185 return labels.Deserialize(label_list); | |
186 } | |
187 | |
188 bool ExperimentLabels::IsLabelContentValid(const CString& str) { | |
189 if (str.IsEmpty()) { | |
190 return false; | |
191 } | |
192 for (int i = 0; i < str.GetLength(); ++i) { | |
193 wchar_t ch = str[i]; | |
194 if (!((ch >= L'+' && ch <= L'-') || | |
195 (ch >= L'0' && ch <= L':') || | |
196 (ch >= L'A' && ch <= L'Z') || | |
197 (ch >= L'a' && ch <= L'z') || | |
198 (ch == L'_') || (ch == L' '))) { | |
199 return false; | |
200 } | |
201 } | |
202 return true; | |
203 } | |
204 | |
205 bool ExperimentLabels::SplitCombinedLabel(const CString& combined, CString* key, | |
206 CString* value, time64* expiration) { | |
207 ASSERT1(!combined.IsEmpty()); | |
208 ASSERT1(key); | |
209 ASSERT1(value); | |
210 ASSERT1(expiration); | |
211 | |
212 int value_offset = combined.Find(L'='); | |
213 if (value_offset <= 0 || value_offset == combined.GetLength()) { | |
214 return false; | |
215 } | |
216 *key = combined.Left(value_offset); | |
217 if (!IsLabelContentValid(*key)) { | |
218 return false; | |
219 } | |
220 ++value_offset; | |
221 | |
222 int expiration_offset = combined.Find(L'|', value_offset); | |
223 if (expiration_offset <= value_offset || | |
224 expiration_offset == combined.GetLength()) { | |
225 return false; | |
226 } | |
227 *value = combined.Mid(value_offset, expiration_offset - value_offset); | |
228 if (!IsLabelContentValid(*value)) { | |
229 return false; | |
230 } | |
231 ++expiration_offset; | |
232 | |
233 CString expiration_string = combined.Mid(expiration_offset); | |
234 if (!IsLabelContentValid(expiration_string)) { | |
235 return false; | |
236 } | |
237 SYSTEMTIME system_time = {}; | |
238 if (!RFC822DateToSystemTime(expiration_string, &system_time, false)) { | |
239 return false; | |
240 } | |
241 *expiration = SystemTimeToTime64(&system_time); | |
242 | |
243 return true; | |
244 } | |
245 | |
246 bool ExperimentLabels::DoDeserialize(LabelMap* map, const CString& label_list, | |
247 bool accept_expired) { | |
248 ASSERT1(map); | |
249 | |
250 if (label_list.IsEmpty()) { | |
251 return true; | |
252 } | |
253 | |
254 time64 current_time = GetCurrent100NSTime(); | |
255 | |
256 for (int offset = 0;;) { | |
257 CString combined_label = label_list.Tokenize(L";", offset); | |
258 if (combined_label.IsEmpty()) { | |
259 if (offset < 0) { | |
260 break; // Natural end-of-string reached. | |
261 } | |
262 return false; | |
263 } | |
264 | |
265 CString key; | |
266 CString value; | |
267 time64 expiration = 0; | |
268 if (!SplitCombinedLabel(combined_label, &key, &value, &expiration)) { | |
269 return false; | |
270 } | |
271 | |
272 // If the label is well-formatted but expired, we accept the input, but | |
273 // do not add it to the map and do not emit an error. If there is already | |
274 // a label in the map with that key, delete it. | |
275 if (accept_expired || expiration > current_time) { | |
276 (*map)[key] = std::make_pair(value, expiration); | |
277 } else { | |
278 map->erase(key); | |
279 } | |
280 } | |
281 | |
282 return true; | |
283 } | |
284 | |
285 } // namespace omaha | |
286 | |
OLD | NEW |