mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
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:
parent
9ff28e51b0
commit
c68dd19ade
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)))
|
||||
|
||||
|
@ -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")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -207,6 +207,7 @@ class InterpreterTest extends WordSpec with Matchers with TableDrivenPropertyChe
|
||||
),
|
||||
),
|
||||
Set.empty[PackageId],
|
||||
None,
|
||||
),
|
||||
),
|
||||
).right.get
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
))
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 <~ `;`) <~ `}` ^^ {
|
||||
|
Loading…
Reference in New Issue
Block a user