Decode and validate package metadata in Scala (#4653)

This adds the code for decoding and validating package metadata
that is now emitted by damlc.

changelog_begin
changelog_end
This commit is contained in:
Moritz Kiefer 2020-02-21 19:20:42 +01:00 committed by GitHub
parent 9ff28e51b0
commit c68dd19ade
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 176 additions and 12 deletions

View File

@ -117,7 +117,7 @@ newtype PackageName = PackageName{unPackageName :: T.Text}
-- | Human-readable version of a package. Must match the regex
--
-- > [0-9]+(\.[0-9]+)*
-- > (0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*
newtype PackageVersion = PackageVersion{unPackageVersion :: T.Text}
deriving stock (Eq, Data, Generic, Ord, Show)
deriving newtype (Hashable, NFData, FromJSON)

View File

@ -147,7 +147,7 @@ class Context(val contextId: Context.ContextId) {
}
def allPackages: Map[PackageId, Ast.Package] =
extPackages + (homePackageId -> Ast.Package(modules, extPackages.keySet))
extPackages + (homePackageId -> Ast.Package(modules, extPackages.keySet, None))
private def buildMachine(identifier: Identifier): Option[Speedy.Machine] = {
for {

View File

@ -40,6 +40,17 @@ private[archive] class DecodeV1(minor: LV.Minor) extends Decode.OfPackage[PLF.Pa
val dependencyTracker = new PackageDependencyTracker(packageId)
val metadata: Option[PackageMetadata] =
if (lfPackage.hasMetadata) {
assertSince(LV.Features.packageMetadata, "Package.metadata")
Some(decodePackageMetadata(lfPackage.getMetadata, internedStrings))
} else {
if (!versionIsOlderThan(LV.Features.packageMetadata)) {
throw ParseError(s"Package.metadata is required in DAML-LF 1.$minor")
}
None
}
Package(
modules = lfPackage.getModulesList.asScala
.map(
@ -50,11 +61,25 @@ private[archive] class DecodeV1(minor: LV.Minor) extends Decode.OfPackage[PLF.Pa
Some(dependencyTracker),
_,
onlySerializableDataDefs).decode),
directDeps = dependencyTracker.getDependencies
directDeps = dependencyTracker.getDependencies,
metadata = metadata,
)
}
private[archive] def decodePackageMetadata(
metadata: PLF.PackageMetadata,
internedStrings: ImmArraySeq[String]): PackageMetadata = {
def getInternedStr(id: Int) =
internedStrings.lift(id).getOrElse {
throw ParseError(s"invalid internedString table index $id")
}
PackageMetadata(
toPackageName(getInternedStr(metadata.getNameInternedStr), "PackageMetadata.name"),
toPackageVersion(getInternedStr(metadata.getVersionInternedStr), "PackageMetadata.version22")
)
}
// each LF scenario module is wrapped in a distinct proto package
type ProtoScenarioModule = PLF.Package
@ -1201,6 +1226,16 @@ private[archive] class DecodeV1(minor: LV.Minor) extends Decode.OfPackage[PLF.Pa
private[this] def toName(s: String): Name =
eitherToParseError(Name.fromString(s))
private[this] def toPackageName(s: String, description: => String): PackageName = {
assertSince(LV.Features.packageMetadata, description)
eitherToParseError(PackageName.fromString(s))
}
private[this] def toPackageVersion(s: String, description: => String): PackageVersion = {
assertSince(LV.Features.packageMetadata, description)
eitherToParseError(PackageVersion.fromString(s))
}
private[this] def toPLNumeric(s: String) =
PLNumeric(eitherToParseError(Numeric.fromString(s)))

View File

@ -94,6 +94,16 @@ class DecodeV1Spec
LV.Minor.Dev,
)
private val prePackageMetadataVersions = Table(
"minVersion",
List(1, 4, 6, 7).map(i => LV.Minor.Stable(i.toString)): _*
)
private val postPackageMetadataVersions = Table(
"minVersion",
LV.Minor.Dev
)
"decodeKind" should {
"reject nat kind if lf version < 1.7" in {
@ -570,4 +580,76 @@ class DecodeV1Spec
}
}
"decodePackageMetadata" should {
"accept a valid package name and version" in {
new DecodeV1(LV.Minor.Dev).decodePackageMetadata(
DamlLf1.PackageMetadata.newBuilder().setNameInternedStr(0).setVersionInternedStr(1).build(),
ImmArraySeq("foobar", "0.0.0")) shouldBe Ast.PackageMetadata(
Ref.PackageName.assertFromString("foobar"),
Ref.PackageVersion.assertFromString("0.0.0"))
}
"reject a package namewith space" in {
a[ParseError] shouldBe thrownBy(new DecodeV1(LV.Minor.Dev).decodePackageMetadata(
DamlLf1.PackageMetadata.newBuilder().setNameInternedStr(0).setVersionInternedStr(1).build(),
ImmArraySeq("foo bar", "0.0.0")))
}
"reject a package version with leading zero" in {
a[ParseError] shouldBe thrownBy(new DecodeV1(LV.Minor.Dev).decodePackageMetadata(
DamlLf1.PackageMetadata.newBuilder().setNameInternedStr(0).setVersionInternedStr(1).build(),
ImmArraySeq("foobar", "01.0.0")))
}
"reject a package version with a dash" in {
a[ParseError] shouldBe thrownBy(new DecodeV1(LV.Minor.Dev).decodePackageMetadata(
DamlLf1.PackageMetadata.newBuilder().setNameInternedStr(0).setVersionInternedStr(1).build(),
ImmArraySeq("foobar", "0.0.0-")))
}
}
"decodePackage" should {
"reject PackageMetadata if lf version < 1.7" in {
forEvery(prePackageMetadataVersions) { minVersion =>
val decoder = new DecodeV1(minVersion)
val pkgId = Ref.PackageId.assertFromString(
"0000000000000000000000000000000000000000000000000000000000000000")
val metadata =
DamlLf1.PackageMetadata.newBuilder.setNameInternedStr(0).setVersionInternedStr(1).build()
val pkg = DamlLf1.Package
.newBuilder()
.addInternedStrings("foobar")
.addInternedStrings("0.0.0")
.setMetadata(metadata)
.build()
a[ParseError] shouldBe thrownBy(decoder.decodePackage(pkgId, pkg, false))
}
}
"require PackageMetadata to be present if lf version >= 1.dev" in {
forEvery(postPackageMetadataVersions) { minVersion =>
val decoder = new DecodeV1(minVersion)
val pkgId = Ref.PackageId.assertFromString(
"0000000000000000000000000000000000000000000000000000000000000000")
a[ParseError] shouldBe thrownBy(
decoder.decodePackage(pkgId, DamlLf1.Package.newBuilder().build(), false))
}
}
"decode PackageMetadata if lf version >= 1.dev" in {
forEvery(postPackageMetadataVersions) { minVersion =>
val decoder = new DecodeV1(minVersion)
val pkgId = Ref.PackageId.assertFromString(
"0000000000000000000000000000000000000000000000000000000000000000")
val metadata =
DamlLf1.PackageMetadata.newBuilder.setNameInternedStr(0).setVersionInternedStr(1).build()
val pkg = DamlLf1.Package
.newBuilder()
.addInternedStrings("foobar")
.addInternedStrings("0.0.0")
.setMetadata(metadata)
.build()
decoder.decodePackage(pkgId, pkg, false).metadata shouldBe Some(
Ast.PackageMetadata(
Ref.PackageName.assertFromString("foobar"),
Ref.PackageVersion.assertFromString("0.0.0")))
}
}
}
}

View File

@ -75,6 +75,10 @@ sealed abstract class IdString {
// In a language like C# you'll need to use some other unicode char for `$`.
type Name <: String
// Human-readable package names and versions.
type PackageName <: String
type PackageVersion <: String
/** Party identifiers are non-empty US-ASCII strings built from letters, digits, space, colon, minus and,
underscore. We use them to represent [Party] literals. In this way, we avoid
empty identifiers, escaping problems, and other similar pitfalls.
@ -98,6 +102,8 @@ sealed abstract class IdString {
type ContractIdString <: String
val Name: StringModule[Name]
val PackageName: ConcatenableStringModule[PackageName]
val PackageVersion: StringModule[PackageVersion]
val Party: ConcatenableStringModule[Party]
val PackageId: ConcatenableStringModule[PackageId]
val ParticipantId: StringModule[ParticipantId]
@ -190,6 +196,19 @@ private[data] final class IdStringImpl extends IdString {
override val Name: StringModule[Name] =
new MatchingStringModule("""[A-Za-z\$_][A-Za-z0-9\$_]*""")
/** Package names are non-empty US-ASCII strings built from letters, digits, minus and underscore.
*/
override type PackageName = String
override val PackageName: ConcatenableStringModule[PackageName] =
new ConcatenableMatchingStringModule("-_".contains(_))
/** Package versions are non-empty strings consisting of segments of digits (without leading zeros)
separated by dots.
*/
override type PackageVersion = String
override val PackageVersion: StringModule[PackageVersion] =
new MatchingStringModule("""(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*""")
/** Party identifiers are non-empty US-ASCII strings built from letters, digits, space, colon, minus and,
*underscore. We use them to represent [Party] literals. In this way, we avoid
* empty identifiers, escaping problems, and other similar pitfalls.

View File

@ -14,6 +14,12 @@ object Ref {
val Name: IdString.Name.type = IdString.Name
implicit def `Name equal instance`: Equal[Name] = Name.equalInstance
type PackageName = IdString.PackageName
val PackageName: IdString.PackageName.type = IdString.PackageName
type PackageVersion = IdString.PackageVersion
val PackageVersion: IdString.PackageVersion.type = IdString.PackageVersion
/** Party identifiers are non-empty US-ASCII strings built from letters, digits, space, colon, minus and,
underscore. We use them to represent [Party] literals. In this way, we avoid
empty identifiers, escaping problems, and other similar pitfalls.

View File

@ -57,7 +57,7 @@ private[digitalasset] object DamlLfEncoder extends App {
val modules = parseModules[this.type](source).fold(error, identity)
val pkgs = Map(pkgId -> Ast.Package(modules, Set.empty[Ref.PackageId]))
val pkgs = Map(pkgId -> Ast.Package(modules, Set.empty[Ref.PackageId], None))
Validation.checkPackage(pkgs, pkgId).left.foreach(e => error(e.pretty))

View File

@ -46,6 +46,14 @@ private[digitalasset] class EncodeV1(val minor: LV.Minor) {
}
}
if (!versionIsOlderThan(LV.Features.packageMetadata)) {
// We just set the metadata to dummy values here.
val metadataBuilder = PLF.PackageMetadata.newBuilder
metadataBuilder.setNameInternedStr(stringsTable.insert("foobar"))
metadataBuilder.setVersionInternedStr(stringsTable.insert("0.0.0"))
builder.setMetadata(metadataBuilder.build)
}
if (!versionIsOlderThan(LV.Features.internedPackageId))
stringsTable.build.foreach(builder.addInternedStrings)

View File

@ -164,7 +164,7 @@ class EncodeV1Spec extends WordSpec with Matchers with TableDrivenPropertyChecks
implicit val parserParameters: ParserParameters[version.type] =
ParserParameters(pkgId, version)
val pkg = Package(parseModules(text).right.get, Set.empty)
val pkg = Package(parseModules(text).right.get, Set.empty, None)
val archive = Encode.encodeArchive(pkgId -> pkg, version)
val ((hashCode @ _, decodedPackage: Package), _) = Decode.readArchiveAndVersion(archive)

View File

@ -207,6 +207,7 @@ class InterpreterTest extends WordSpec with Matchers with TableDrivenPropertyChe
),
),
Set.empty[PackageId],
None,
),
),
).right.get

View File

@ -666,7 +666,12 @@ object Ast {
}
}
case class Package(modules: Map[ModuleName, Module], directDeps: Set[PackageId]) {
case class PackageMetadata(name: PackageName, version: PackageVersion)
case class Package(
modules: Map[ModuleName, Module],
directDeps: Set[PackageId],
metadata: Option[PackageMetadata]) {
def lookupIdentifier(identifier: QualifiedName): Either[String, Definition] = {
this.modules.get(identifier.module) match {
case None =>
@ -685,12 +690,15 @@ object Ast {
object Package {
def apply(modules: Traversable[Module], directDeps: Traversable[PackageId]): Package = {
def apply(
modules: Traversable[Module],
directDeps: Traversable[PackageId],
metadata: Option[PackageMetadata]): Package = {
val modulesWithNames = modules.map(m => m.name -> m)
findDuplicate(modulesWithNames).foreach { modName =>
throw PackageError(s"Collision on module name ${modName.toString}")
}
Package(modulesWithNames.toMap, directDeps.toSet)
Package(modulesWithNames.toMap, directDeps.toSet, metadata)
}
}

View File

@ -64,6 +64,7 @@ object LanguageVersion {
val genMap = v1_dev
val typeSynonyms = v1_dev
val scenarioMustFailAtMsg = v1_dev
val packageMetadata = v1_dev
/** Unstable, experimental features. This should stay in 1.dev forever.
* Features implemented with this flag should be moved to a separate

View File

@ -22,14 +22,16 @@ class AstSpec extends WordSpec with TableDrivenPropertyChecks with Matchers {
List(
Module(modName1, List.empty, List.empty, defaultVersion, FeatureFlags.default),
Module(modName2, List.empty, List.empty, defaultVersion, FeatureFlags.default)),
Set.empty
Set.empty,
None
)
a[PackageError] shouldBe thrownBy(
Package(
List(
Module(modName1, List.empty, List.empty, defaultVersion, FeatureFlags.default),
Module(modName1, List.empty, List.empty, defaultVersion, FeatureFlags.default)),
Set.empty
Set.empty,
None
))
}

View File

@ -25,7 +25,9 @@ private[digitalasset] class AstRewriter(
}
.toSeq
.toMap,
Set.empty[PackageId])
Set.empty[PackageId],
pkg.metadata,
)
def apply(module: Module): Module =
module match {

View File

@ -29,7 +29,7 @@ private[parser] class ModParser[P](parameters: ParserParameters[P]) {
}
lazy val pkg: Parser[Package] =
rep(mod) ^^ (Package(_, Set.empty))
rep(mod) ^^ (Package(_, Set.empty, None))
lazy val mod: Parser[Module] =
Id("module") ~! tags(modTags) ~ dottedName ~ `{` ~ rep(definition <~ `;`) <~ `}` ^^ {