File
Blob: src/workerd/io/compatibility-date.c++
| 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 | |
| 20 | namespace workerd { |
| 21 | |
| 22 | using kj::uint; |
| 23 | |
| 24 | namespace { |
| 25 | |
| 26 | struct 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 | |
| 98 | kj::String currentDateStr() { |
| 99 | return CompatDate::today().toString(); |
| 100 | } |
| 101 | |
| 102 | static 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 | |
| 263 | void 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 | |
| 281 | void 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 | |
| 299 | namespace { |
| 300 | |
| 301 | struct ParsedField { |
| 302 | kj::StringPtr enableFlag; |
| 303 | capnp::StructSchema::Field field; |
| 304 | }; |
| 305 | |
| 306 | kj::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 | |
| 334 | kj::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 | |
| 349 | kj::Maybe<kj::String> normalizeCompatDate(kj::StringPtr date) { |
| 350 | return CompatDate::parse(date).map([](auto v) { return v.toString(); }); |
| 351 | } |
| 352 | |
| 353 | } // namespace workerd |