Skip to content
File

Blob: src/workerd/io/compatibility-date.c++

12.8 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "compatibility-date.h"
6 
7#include "time.h"
8 
9#include <workerd/io/maximum-compatibility-date.embed.h>
10#include <workerd/io/release-version.embed.h>
11 
12#include <capnp/dynamic.h>
13#include <capnp/schema.h>
14#include <kj/debug.h>
15#include <kj/map.h>
16#include <kj/vector.h>
17 
18#include <cstdio>
19 
20namespace workerd {
21 
22using kj::uint;
23 
24namespace {
25 
26struct CompatDate {
27 uint year;
28 uint month;
29 uint day;
30 
31 inline bool operator==(const CompatDate& other) const {
32 return year == other.year && month == other.month && day == other.day;
33 }
34 inline bool operator<(const CompatDate& other) const {
35 if (year < other.year) return true;
36 if (year > other.year) return false;
37 if (month < other.month) return true;
38 if (month > other.month) return false;
39 return day < other.day;
40 }
41 inline bool operator>=(const CompatDate& other) const {
42 return !(*this < other);
43 }
44 
45 static kj::Maybe<CompatDate> parse(kj::StringPtr text) {
46 // Basic sanity check that years are 4-digit in the [2000,2999] range. If it is the year 3000
47 // and this code broke, all I can say is: haha, take that robots, humans screwed you over yet
48 // again, you can Roko's basilisk me all you want I don't care.
49 if (!text.startsWith("2")) return kj::none;
50 // Force 4-digit year, 2-digit month, and 2-digit day.
51 if (text.size() != 10 || text[4] != '-' || text[7] != '-') {
52 return kj::none;
53 }
54 // Validate the date contains only digits and dashes.
55 for (char c: text) {
56 if ((c < '0' || '9' < c) && c != '-') return kj::none;
57 }
58 uint year, month, day;
59 // TODO(someday): use `kj::parse` here instead
60 auto result = sscanf(text.cStr(), "%d-%d-%d", &year, &month, &day);
61 if (result == EOF || result < 3) return kj::none;
62 // Basic validation, notably this will happily accept invalid dates like 2022-02-30
63 if (year < 2000 || year >= 3000) return kj::none;
64 if (month < 1 || month > 12) return kj::none;
65 if (day < 1 || day > 31) return kj::none;
66 return CompatDate{year, month, day};
67 }
68 
69 static CompatDate parse(kj::StringPtr text, Worker::ValidationErrorReporter& errorReporter) {
70 static constexpr CompatDate DEFAULT_DATE{2021, 5, 1};
71 KJ_IF_SOME(v, parse(text)) {
72 return v;
73 } else {
74 errorReporter.addError(kj::str("Invalid compatibility date: ", text));
75 return DEFAULT_DATE;
76 }
77 }
78 
79 static CompatDate today() {
80 time_t now = time(nullptr);
81#if _MSC_VER
82 // `gmtime` is thread-safe on Windows: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/gmtime-gmtime32-gmtime64?view=msvc-170#return-value
83 auto t = *gmtime(&now);
84#else
85 struct tm t;
86 KJ_ASSERT(gmtime_r(&now, &t) == &t);
87#endif
88 return {static_cast<uint>(t.tm_year + 1900), static_cast<uint>(t.tm_mon + 1),
89 static_cast<uint>(t.tm_mday)};
90 }
91 
92 kj::String toString() {
93 return kj::str(year, '-', month < 10 ? "0" : "", month, '-', day < 10 ? "0" : "", day);
94 }
95};
96} // namespace
97 
98kj::String currentDateStr() {
99 return CompatDate::today().toString();
100}
101 
102static void compileCompatibilityFlags(kj::StringPtr compatDate,
103 kj::HashSet<kj::String> flagSet,
104 CompatibilityFlags::Builder output,
105 Worker::ValidationErrorReporter& errorReporter,
106 bool allowExperimentalFeatures,
107 CompatibilityDateValidation dateValidation) {
108 auto parsedCompatDate = CompatDate::parse(compatDate, errorReporter);
109 
110 switch (dateValidation) {
111 case CompatibilityDateValidation::CODE_VERSION:
112 if (KJ_ASSERT_NONNULL(CompatDate::parse(MAXIMUM_COMPATIBILITY_DATE)) < parsedCompatDate) {
113 errorReporter.addError(
114 kj::str("This Worker requires compatibility date \"", parsedCompatDate,
115 "\", but the newest "
116 "date supported by this server binary is \"",
117 MAXIMUM_COMPATIBILITY_DATE, "\"."));
118 }
119 // workerd is built with MAXIMUM_COMPATIBILITY_DATE set a little bit into the future, so
120 // that the build can support setting the compat date to today until the next release is
121 // ready. But we don't want people to actually set their compat date in the future, so let's
122 // check against the clock time as well.
123 if (CompatDate::today() < parsedCompatDate) {
124 errorReporter.addError(kj::str("Can't set compatibility date in the future: \"",
125 parsedCompatDate, "\". Today's date (UTC) is \"", CompatDate::today(), "\"."));
126 }
127 break;
128 
129 case CompatibilityDateValidation::CURRENT_DATE_FOR_CLOUDFLARE:
130 if (CompatDate::today() < parsedCompatDate) {
131 errorReporter.addError(
132 kj::str("Can't set compatibility date in the future: ", parsedCompatDate));
133 }
134 break;
135 
136 case CompatibilityDateValidation::FUTURE_FOR_TEST:
137 // No validation.
138 break;
139 }
140 
141 auto schema = capnp::Schema::from<CompatibilityFlags>();
142 auto dynamicOutput = capnp::toDynamic(output);
143 
144 // For each item added to this list, the flag identified by field will be
145 // enabled if the flag identified by other is enabled.
146 struct ImpliedBy {
147 capnp::StructSchema::Field field;
148 capnp::StructSchema::Field other;
149 };
150 kj::Vector<ImpliedBy> impliedByList(schema.getFields().size());
151 
152 for (auto field: schema.getFields()) {
153 bool enableByDate = false;
154 bool enableByFlag = false;
155 bool disableByFlag = false;
156 bool isExperimental = false;
157 
158 kj::Maybe<CompatDate> enableDate;
159 kj::StringPtr enableFlagName;
160 kj::StringPtr disableFlagName;
161 kj::Vector<ImpliedBy> impliedByVector;
162 
163 for (auto annotation: field.getProto().getAnnotations()) {
164 if (annotation.getId() == COMPAT_ENABLE_FLAG_ANNOTATION_ID) {
165 enableFlagName = annotation.getValue().getText();
166 KJ_IF_SOME(entry, flagSet.find(enableFlagName)) {
167 enableByFlag = true;
168 flagSet.erase(entry);
169 }
170 } else if (annotation.getId() == COMPAT_DISABLE_FLAG_ANNOTATION_ID) {
171 disableFlagName = annotation.getValue().getText();
172 KJ_IF_SOME(entry, flagSet.find(disableFlagName)) {
173 disableByFlag = true;
174 flagSet.erase(entry);
175 }
176 } else if (annotation.getId() == COMPAT_ENABLE_DATE_ANNOTATION_ID) {
177 auto parsedDate = KJ_ASSERT_NONNULL(CompatDate::parse(annotation.getValue().getText()));
178 enableDate = parsedDate;
179 enableByDate = parsedCompatDate >= parsedDate;
180 } else if (annotation.getId() == COMPAT_ENABLE_ALL_DATES_ANNOTATION_ID) {
181 enableByDate = true;
182 } else if (annotation.getId() == EXPERIMENTAl_ANNOTATION_ID) {
183 isExperimental = true;
184 } else if (annotation.getId() == IMPLIED_BY_AFTER_DATE_ANNOTATION_ID) {
185 auto value = annotation.getValue();
186 auto s = value.getStruct().as<workerd::ImpliedByAfterDate>();
187 auto parsedDate = KJ_ASSERT_NONNULL(CompatDate::parse(s.getDate()));
188 // This flag will be marked as enabled if the flag identified by
189 // s.getName() is enabled, but only on or after the specified date.
190 if (parsedCompatDate >= parsedDate && !disableByFlag) {
191 if (s.hasName()) {
192 impliedByVector.add(ImpliedBy{
193 .field = field,
194 .other = schema.getFieldByName(s.getName()),
195 });
196 } else if (s.hasNames()) {
197 for (auto name: s.getNames()) {
198 impliedByVector.add(ImpliedBy{
199 .field = field,
200 .other = schema.getFieldByName(name),
201 });
202 }
203 }
204 }
205 }
206 }
207 for (auto& impliedBy: impliedByVector) {
208 // We only want to add the implied by flag if it is not explicitly disabled.
209 if (!disableByFlag) {
210 impliedByList.add(kj::mv(impliedBy));
211 }
212 }
213 
214 // Check for conflicts.
215 if (enableByFlag && disableByFlag) {
216 errorReporter.addError(kj::str("Compatibility flags are mutually contradictory: ",
217 enableFlagName, " vs ", disableFlagName));
218 }
219 if (enableByFlag && enableByDate &&
220 dateValidation != CompatibilityDateValidation::FUTURE_FOR_TEST) {
221 // Skip this error for FUTURE_FOR_TEST since tests may need to explicitly specify flags
222 // for the default variant (which uses an old compat date) while the all-compat-flags
223 // variant enables all flags by date.
224 KJ_IF_SOME(d, enableDate) {
225 errorReporter.addError(kj::str("The compatibility flag ", enableFlagName,
226 " became the default as of ", d, " so does not need to be specified anymore."));
227 } else {
228 errorReporter.addError(kj::str("The compatibility flag ", enableFlagName,
229 " is the default, so does not need to be specified anymore."));
230 }
231 }
232 if (disableByFlag && !enableByDate) {
233 // We don't consider it an error to specify a disable flag when the compatibility date makes
234 // it redundant, because at a future date it won't be redundant, and someone could want to
235 // set the flag early to make sure they don't forget later.
236 }
237 if (enableByFlag && isExperimental && !allowExperimentalFeatures) {
238 if (dateValidation == CompatibilityDateValidation::CURRENT_DATE_FOR_CLOUDFLARE) {
239 errorReporter.addError(kj::str("The compatibility flag ", enableFlagName,
240 " is experimental and cannot yet be used in Workers deployed to Cloudflare."));
241 } else {
242 errorReporter.addError(kj::str("The compatibility flag ", enableFlagName,
243 " is experimental and may break or be "
244 "removed in a future version of workerd. To use this flag, you must pass --experimental "
245 "on the command line."));
246 }
247 }
248 
249 dynamicOutput.set(field, enableByFlag || (enableByDate && !disableByFlag));
250 }
251 
252 for (auto& implied: impliedByList) {
253 if (capnp::toDynamic(output).get(implied.other).as<bool>()) {
254 dynamicOutput.set(implied.field, true);
255 }
256 }
257 
258 for (auto& flag: flagSet) {
259 errorReporter.addError(kj::str("No such compatibility flag: ", flag));
260 }
261}
262 
263void compileCompatibilityFlags(kj::StringPtr compatDate,
264 capnp::List<capnp::Text>::Reader compatFlags,
265 CompatibilityFlags::Builder output,
266 Worker::ValidationErrorReporter& errorReporter,
267 bool allowExperimentalFeatures,
268 CompatibilityDateValidation dateValidation) {
269 kj::HashSet<kj::String> flagSet;
270 flagSet.reserve(compatFlags.size());
271 for (auto flag: compatFlags) {
272 flagSet.upsert(kj::str(flag), [&](auto& existing, auto&& newValue) {
273 errorReporter.addError(kj::str("Compatibility flag specified multiple times: ", flag));
274 });
275 }
276 
277 return compileCompatibilityFlags(compatDate, kj::mv(flagSet), output, errorReporter,
278 allowExperimentalFeatures, dateValidation);
279}
280 
281void compileCompatibilityFlags(kj::StringPtr compatDate,
282 kj::ArrayPtr<const kj::String> compatFlags,
283 CompatibilityFlags::Builder output,
284 Worker::ValidationErrorReporter& errorReporter,
285 bool allowExperimentalFeatures,
286 CompatibilityDateValidation dateValidation) {
287 kj::HashSet<kj::String> flagSet;
288 flagSet.reserve(compatFlags.size());
289 for (auto& flag: compatFlags) {
290 flagSet.upsert(kj::str(flag), [&](auto& existing, auto&& newValue) {
291 errorReporter.addError(kj::str("Compatibility flag specified multiple times: ", flag));
292 });
293 }
294 
295 return compileCompatibilityFlags(compatDate, kj::mv(flagSet), output, errorReporter,
296 allowExperimentalFeatures, dateValidation);
297}
298 
299namespace {
300 
301struct ParsedField {
302 kj::StringPtr enableFlag;
303 capnp::StructSchema::Field field;
304};
305 
306kj::Array<const ParsedField> makeFieldTable(capnp::StructSchema::FieldList fields) {
307 kj::Vector<ParsedField> table(fields.size());
308 
309 for (auto field: fields) {
310 kj::Maybe<kj::StringPtr> enableFlag;
311 bool neededByFl = false;
312 
313 for (auto annotation: field.getProto().getAnnotations()) {
314 if (annotation.getId() == COMPAT_ENABLE_FLAG_ANNOTATION_ID) {
315 enableFlag = annotation.getValue().getText();
316 } else if (annotation.getId() == NEEDED_BY_FL) {
317 neededByFl = true;
318 }
319 }
320 
321 if (neededByFl) {
322 table.add(ParsedField{
323 .enableFlag = KJ_REQUIRE_NONNULL(enableFlag),
324 .field = field,
325 });
326 }
327 }
328 
329 return table.releaseAsArray();
330}
331 
332} // namespace
333 
334kj::Array<kj::StringPtr> decompileCompatibilityFlagsForFl(CompatibilityFlags::Reader input) {
335 static const auto fieldTable =
336 makeFieldTable(capnp::Schema::from<CompatibilityFlags>().getFields());
337 
338 kj::Vector<kj::StringPtr> enableFlags;
339 enableFlags.reserve(fieldTable.size());
340 for (auto field: fieldTable) {
341 if (capnp::toDynamic(input).get(field.field).as<bool>()) {
342 enableFlags.add(field.enableFlag);
343 }
344 }
345 
346 return enableFlags.releaseAsArray();
347}
348 
349kj::Maybe<kj::String> normalizeCompatDate(kj::StringPtr date) {
350 return CompatDate::parse(date).map([](auto v) { return v.toString(); });
351}
352 
353} // namespace workerd