Skip to content

Sema: Avoid decoding unavailable enum elements in derived Codable conformances #67556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/swift/AST/KnownIdentifiers.def
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ IDENTIFIER(CVarArg)
IDENTIFIER(Cxx)
IDENTIFIER(CxxStdlib)
IDENTIFIER(Darwin)
IDENTIFIER(dataCorrupted)
IDENTIFIER(Distributed)
IDENTIFIER(dealloc)
IDENTIFIER(debugDescription)
Expand Down
49 changes: 43 additions & 6 deletions lib/Sema/DerivedConformanceCodable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1110,10 +1110,19 @@ deriveBodyEncodable_enum_encode(AbstractFunctionDecl *encodeDecl, void *) {
auto switchStmt = createEnumSwitch(
C, funcDC, enumRef, enumDecl, codingKeysEnum,
/*createSubpattern*/ true,
[&](auto *elt, auto *codingKeyCase,
auto payloadVars) -> std::tuple<EnumElementDecl *, BraceStmt *> {
[&](EnumElementDecl *elt, EnumElementDecl *codingKeyCase,
ArrayRef<VarDecl *> payloadVars)
-> std::tuple<EnumElementDecl *, BraceStmt *> {
SmallVector<ASTNode, 3> caseStatements;

if (elt->getAttrs().isUnavailable(C)) {
// This case is not encodable because it is unavailable and therefore
// should not be instantiable at runtime. Skipping this case will
// result in the SIL pipeline giving the switch a default case for
// unexpected values.
return std::make_tuple(nullptr, nullptr);
}

if (!codingKeyCase) {
// This case should not be encodable, so throw an error if an attempt
// is made to encode it
Expand Down Expand Up @@ -1539,11 +1548,13 @@ deriveBodyDecodable_enum_init(AbstractFunctionDecl *initDecl, void *) {
// enum Foo : Codable {
// case bar(x: Int)
// case baz(y: String)
// @available(*, unavailable) case qux(z: Double)
//
// // Already derived by this point if possible.
// @derived enum CodingKeys : CodingKey {
// case bar
// case baz
// case qux
//
// @derived enum BarCodingKeys : CodingKey {
// case x
Expand All @@ -1552,6 +1563,10 @@ deriveBodyDecodable_enum_init(AbstractFunctionDecl *initDecl, void *) {
// @derived enum BazCodingKeys : CodingKey {
// case y
// }
//
// @derived enum QuxCodingKeys : CodingKey {
// case z
// }
// }
//
// @derived init(from decoder: Decoder) throws {
Expand All @@ -1571,9 +1586,15 @@ deriveBodyDecodable_enum_init(AbstractFunctionDecl *initDecl, void *) {
// self = .bar(x: x)
// case .baz:
// let nestedContainer = try container.nestedContainer(
// keyedBy: BarCodingKeys.self, forKey: .baz)
// keyedBy: BazCodingKeys.self, forKey: .baz)
// let y = try nestedContainer.decode(String.self, forKey: .y)
// self = .baz(y: y)
// case .qux:
// throw DecodingError.dataCorrupted(
// DecodingError.Context(
// codingPath: decoder.codingPath,
// debugDescription: "Unavailable enum element encountered.")
// )
// }
// }

Expand Down Expand Up @@ -1689,14 +1710,30 @@ deriveBodyDecodable_enum_init(AbstractFunctionDecl *initDecl, void *) {
auto switchStmt = createEnumSwitch(
C, funcDC, theKeyExpr, targetEnum, codingKeysEnum,
/*createSubpattern*/ false,
[&](auto *elt, auto *codingKeyCase,
auto payloadVars) -> std::tuple<EnumElementDecl *, BraceStmt *> {
[&](EnumElementDecl *elt, EnumElementDecl *codingKeyCase,
ArrayRef<VarDecl *> payloadVars)
-> std::tuple<EnumElementDecl *, BraceStmt *> {
// Skip this case if it's not defined in the CodingKeys
if (!codingKeyCase)
return std::make_tuple(nullptr, nullptr);

llvm::SmallVector<ASTNode, 3> caseStatements;
if (elt->getAttrs().isUnavailable(C)) {
// generate:
// throw DecodingError.dataCorrupted(
// DecodingError.Context(
// codingPath: decoder.codingPath,
// debugDescription: "...")
auto *throwStmt = createThrowCodingErrorStmt(
C, containerExpr, C.getDecodingErrorDecl(), C.Id_dataCorrupted,
llvm::None, "Unavailable enum element encountered.");

auto body =
BraceStmt::create(C, SourceLoc(), {throwStmt}, SourceLoc());

return std::make_tuple(codingKeyCase, body);
}

llvm::SmallVector<ASTNode, 3> caseStatements;
auto caseIdentifier = caseCodingKeysIdentifier(C, elt);
auto *caseCodingKeys =
lookupEvaluatedCodingKeysEnum(C, targetEnum, caseIdentifier);
Expand Down
5 changes: 5 additions & 0 deletions lib/Sema/DerivedConformanceCodingKey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ deriveBodyCodingKey_init_stringValue(AbstractFunctionDecl *initDecl, void *) {
auto *selfRef = DerivedConformance::createSelfDeclRef(initDecl);
SmallVector<ASTNode, 4> cases;
for (auto *elt : elements) {
// Skip the cases that would return unavailable elements since those can't
// be instantiated at runtime.
if (elt->getAttrs().isUnavailable(C))
continue;

auto *litExpr = new (C) StringLiteralExpr(elt->getNameStr(), SourceRange(),
/*Implicit=*/true);
auto *litPat = ExprPattern::createImplicit(C, litExpr, /*DC*/ initDecl);
Expand Down
4 changes: 4 additions & 0 deletions test/decl/protocol/special/coding/enum_codable_simple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum SimpleEnum : Codable {
case b(z: String)
case c(Int, String, b: Bool)
case d(_ inner: Int)
@available(*, unavailable) case e(Int)

// These lines have to be within the SimpleEnum type because CodingKeys
// should be private.
Expand All @@ -17,6 +18,7 @@ enum SimpleEnum : Codable {
let _ = SimpleEnum.BCodingKeys.self
let _ = SimpleEnum.CCodingKeys.self
let _ = SimpleEnum.DCodingKeys.self
let _ = SimpleEnum.ECodingKeys.self

// The enum should have a case for each of the cases.
let _ = SimpleEnum.CodingKeys.a
Expand All @@ -33,6 +35,8 @@ enum SimpleEnum : Codable {
let _ = SimpleEnum.CCodingKeys.b

let _ = SimpleEnum.DCodingKeys._0

let _ = SimpleEnum.ECodingKeys._0
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// RUN: %target-run-simple-swift
// RUN: %target-run-simple-swift(-unavailable-decl-optimization=complete)

// REQUIRES: executable_test

enum Key: CodingKey {
case a
@available(*, unavailable) case b
}

guard Key(stringValue: "a") == .a else { fatalError() }
guard Key(stringValue: "b") == nil else { fatalError() }

enum StringKey: String, CodingKey {
case x
@available(*, unavailable) case y
}

guard StringKey(stringValue: "x") == .x else { fatalError() }
guard StringKey(stringValue: "y") == nil else { fatalError() }
guard StringKey(intValue: 0) == nil else { fatalError() }
guard StringKey(intValue: 1) == nil else { fatalError() }

enum IntKey: Int, CodingKey {
case zero = 0
@available(*, unavailable) case one = 1
}

guard IntKey(stringValue: "zero") == .zero else { fatalError() }
guard IntKey(stringValue: "one") == nil else { fatalError() }
guard IntKey(intValue: 0) == .zero else { fatalError() }
guard IntKey(intValue: 1) == nil else { fatalError() }
48 changes: 48 additions & 0 deletions test/stdlib/CodableEnumUnavailableElement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// RUN: %target-run-simple-swift
// RUN: %target-run-simple-swift(-unavailable-decl-optimization=complete)
// REQUIRES: executable_test
// REQUIRES: objc_interop

import StdlibUnittest
import Foundation

var CodableEnumUnavailableElementTestSuite = TestSuite("CodableEnumUnavailableElement")

@available(*, unavailable)
struct UnavailableStruct: Codable {}

enum EnumWithUnavailableCase: Codable {
case a
@available(*, unavailable)
case b(UnavailableStruct)
}

func decodeEnum(from json: String) throws -> EnumWithUnavailableCase {
let data = json.data(using: .utf8)!
return try JSONDecoder().decode(EnumWithUnavailableCase.self, from: data)
}

CodableEnumUnavailableElementTestSuite.test("decode_available_case") {
do {
let decoded = try decodeEnum(from: #"{"a":{}}"#)
switch decoded {
case .a: break
default: assertionFailure("Unexpected value \(decoded)")
}
} catch {
expectUnreachable("Unexpected error \(error)")
}
}

CodableEnumUnavailableElementTestSuite.test("decode_unavailable_case") {
do {
let decoded = try decodeEnum(from: #"{"b":{"_0":{}}}"#)
expectUnreachable("Unexpectedly decoded \(decoded)")
} catch DecodingError.dataCorrupted {
// Expected error
} catch {
expectUnreachable("Unexpected error \(error)")
}
}

runAllTests()