Skip to content

Commit bfadfa8

Browse files
authored
Various changes including introducing Format (readium#427)
1 parent c9a09ac commit bfadfa8

File tree

121 files changed

+3760
-3692
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+3760
-3692
lines changed

readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt

+74-33
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,50 @@ package org.readium.r2.lcp
88

99
import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
1010
import org.readium.r2.lcp.license.model.LicenseDocument
11+
import org.readium.r2.shared.publication.encryption.Encryption
1112
import org.readium.r2.shared.publication.encryption.encryption
12-
import org.readium.r2.shared.publication.flatten
13+
import org.readium.r2.shared.publication.epub.EpubEncryptionParser
1314
import org.readium.r2.shared.publication.protection.ContentProtection
1415
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
1516
import org.readium.r2.shared.util.AbsoluteUrl
1617
import org.readium.r2.shared.util.DebugError
1718
import org.readium.r2.shared.util.ThrowableError
1819
import org.readium.r2.shared.util.Try
20+
import org.readium.r2.shared.util.Url
1921
import org.readium.r2.shared.util.asset.Asset
20-
import org.readium.r2.shared.util.asset.AssetRetriever
22+
import org.readium.r2.shared.util.asset.AssetOpener
2123
import org.readium.r2.shared.util.asset.ContainerAsset
2224
import org.readium.r2.shared.util.asset.ResourceAsset
25+
import org.readium.r2.shared.util.data.Container
2326
import org.readium.r2.shared.util.data.ReadError
27+
import org.readium.r2.shared.util.data.decodeRwpm
28+
import org.readium.r2.shared.util.data.decodeXml
29+
import org.readium.r2.shared.util.data.readDecodeOrElse
2430
import org.readium.r2.shared.util.flatMap
31+
import org.readium.r2.shared.util.format.Format
32+
import org.readium.r2.shared.util.format.Trait
2533
import org.readium.r2.shared.util.getOrElse
34+
import org.readium.r2.shared.util.resource.Resource
2635
import org.readium.r2.shared.util.resource.TransformingContainer
2736

2837
internal class LcpContentProtection(
2938
private val lcpService: LcpService,
3039
private val authentication: LcpAuthenticating,
31-
private val assetRetriever: AssetRetriever
40+
private val assetOpener: AssetOpener
3241
) : ContentProtection {
3342

34-
override val scheme: ContentProtection.Scheme =
35-
ContentProtection.Scheme.Lcp
36-
37-
override suspend fun supports(
38-
asset: Asset
39-
): Try<Boolean, Nothing> =
40-
Try.success(lcpService.isLcpProtected(asset))
41-
4243
override suspend fun open(
4344
asset: Asset,
4445
credentials: String?,
4546
allowUserInteraction: Boolean
46-
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
47+
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
48+
if (
49+
!asset.format.conformsTo(Trait.LCP_PROTECTED) &&
50+
!asset.format.conformsTo(Format.LCP_LICENSE_DOCUMENT)
51+
) {
52+
return Try.failure(ContentProtection.OpenError.AssetNotSupported())
53+
}
54+
4755
return when (asset) {
4856
is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction)
4957
is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction)
@@ -54,7 +62,7 @@ internal class LcpContentProtection(
5462
asset: ContainerAsset,
5563
credentials: String?,
5664
allowUserInteraction: Boolean
57-
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
65+
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
5866
val license = retrieveLicense(asset, credentials, allowUserInteraction)
5967
return createResultAsset(asset, license)
6068
}
@@ -71,40 +79,73 @@ internal class LcpContentProtection(
7179
return lcpService.retrieveLicense(asset, authentication, allowUserInteraction)
7280
}
7381

74-
private fun createResultAsset(
82+
private suspend fun createResultAsset(
7583
asset: ContainerAsset,
7684
license: Try<LcpLicense, LcpError>
77-
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
85+
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
7886
val serviceFactory = LcpContentProtectionService
7987
.createFactory(license.getOrNull(), license.failureOrNull())
8088

81-
val decryptor = LcpDecryptor(license.getOrNull())
89+
val encryptionData =
90+
when {
91+
asset.format.conformsTo(Trait.EPUB) -> parseEncryptionDataEpub(asset.container)
92+
else -> parseEncryptionDataRpf(asset.container)
93+
}
94+
.getOrElse { return Try.failure(ContentProtection.OpenError.Reading(it)) }
95+
96+
val decryptor = LcpDecryptor(license.getOrNull(), encryptionData)
8297

8398
val container = TransformingContainer(asset.container, decryptor::transform)
8499

85-
val protectedFile = ContentProtection.Asset(
86-
mediaType = asset.mediaType,
87-
container = container,
100+
val protectedFile = ContentProtection.OpenResult(
101+
asset = ContainerAsset(
102+
format = asset.format,
103+
container = container
104+
),
88105
onCreatePublication = {
89-
decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links)
90-
.flatten()
91-
.mapNotNull {
92-
it.properties.encryption?.let { enc -> it.url() to enc }
93-
}
94-
.toMap()
95-
96106
servicesBuilder.contentProtectionServiceFactory = serviceFactory
97107
}
98108
)
99109

100110
return Try.success(protectedFile)
101111
}
102112

113+
private suspend fun parseEncryptionDataEpub(container: Container<Resource>): Try<Map<Url, Encryption>, ReadError> {
114+
val encryptionResource = container[Url("META-INF/encryption.xml")!!]
115+
?: return Try.failure(ReadError.Decoding("Missing encryption.xml"))
116+
117+
val encryptionDocument = encryptionResource
118+
.readDecodeOrElse(
119+
decode = { it.decodeXml() },
120+
recover = { return Try.failure(it) }
121+
)
122+
123+
return Try.success(EpubEncryptionParser.parse(encryptionDocument))
124+
}
125+
126+
private suspend fun parseEncryptionDataRpf(container: Container<Resource>): Try<Map<Url, Encryption>, ReadError> {
127+
val manifestResource = container[Url("manifest.json")!!]
128+
?: return Try.failure(ReadError.Decoding("Missing manifest"))
129+
130+
val manifest = manifestResource
131+
.readDecodeOrElse(
132+
decode = { it.decodeRwpm() },
133+
recover = { return Try.failure(it) }
134+
)
135+
136+
val encryptionData = manifest
137+
.let { (it.readingOrder + it.resources) }
138+
.mapNotNull { link -> link.properties.encryption?.let { link.url() to it } }
139+
.toMap()
140+
141+
return Try.success(encryptionData)
142+
}
143+
103144
private suspend fun openLicense(
104145
licenseAsset: ResourceAsset,
105146
credentials: String?,
106147
allowUserInteraction: Boolean
107-
): Try<ContentProtection.Asset, ContentProtection.OpenError> {
148+
): Try<ContentProtection.OpenResult, ContentProtection.OpenError> {
108149
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction)
109150

110151
val licenseDoc = license.getOrNull()?.license
@@ -145,14 +186,14 @@ internal class LcpContentProtection(
145186

146187
val asset =
147188
if (link.mediaType != null) {
148-
assetRetriever.retrieve(
189+
assetOpener.open(
149190
url,
150191
mediaType = link.mediaType
151192
)
152193
.map { it as ContainerAsset }
153194
.mapFailure { it.wrap() }
154195
} else {
155-
assetRetriever.retrieve(url)
196+
assetOpener.open(url)
156197
.mapFailure { it.wrap() }
157198
.flatMap {
158199
if (it is ContainerAsset) {
@@ -172,13 +213,13 @@ internal class LcpContentProtection(
172213
return asset.flatMap { createResultAsset(it, license) }
173214
}
174215

175-
private fun AssetRetriever.RetrieveError.wrap(): ContentProtection.OpenError =
216+
private fun AssetOpener.OpenError.wrap(): ContentProtection.OpenError =
176217
when (this) {
177-
is AssetRetriever.RetrieveError.FormatNotSupported ->
218+
is AssetOpener.OpenError.FormatNotSupported ->
178219
ContentProtection.OpenError.AssetNotSupported(this)
179-
is AssetRetriever.RetrieveError.Reading ->
220+
is AssetOpener.OpenError.Reading ->
180221
ContentProtection.OpenError.Reading(cause)
181-
is AssetRetriever.RetrieveError.SchemeNotSupported ->
222+
is AssetOpener.OpenError.SchemeNotSupported ->
182223
ContentProtection.OpenError.AssetNotSupported(this)
183224
}
184225
}

readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import org.readium.r2.shared.util.resource.flatMap
2929
*/
3030
internal class LcpDecryptor(
3131
val license: LcpLicense?,
32-
var encryptionData: Map<Url, Encryption> = emptyMap()
32+
val encryptionData: Map<Url, Encryption>
3333
) {
3434

3535
fun transform(url: Url, resource: Resource): Resource {

readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt

+23-26
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,21 @@ import org.readium.r2.lcp.license.model.LicenseDocument
1515
import org.readium.r2.shared.extensions.tryOrLog
1616
import org.readium.r2.shared.util.AbsoluteUrl
1717
import org.readium.r2.shared.util.ErrorException
18+
import org.readium.r2.shared.util.asset.AssetSniffer
1819
import org.readium.r2.shared.util.downloads.DownloadManager
20+
import org.readium.r2.shared.util.format.Format
21+
import org.readium.r2.shared.util.format.FormatHints
22+
import org.readium.r2.shared.util.format.FormatRegistry
23+
import org.readium.r2.shared.util.format.Trait
1924
import org.readium.r2.shared.util.getOrElse
20-
import org.readium.r2.shared.util.mediatype.FormatRegistry
21-
import org.readium.r2.shared.util.mediatype.MediaType
22-
import org.readium.r2.shared.util.mediatype.MediaTypeHints
23-
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever
2425

2526
/**
2627
* Utility to acquire a protected publication from an LCP License Document.
2728
*/
2829
public class LcpPublicationRetriever(
2930
context: Context,
3031
private val downloadManager: DownloadManager,
31-
private val mediaTypeRetriever: MediaTypeRetriever
32+
private val assetSniffer: AssetSniffer
3233
) {
3334

3435
@JvmInline
@@ -194,19 +195,22 @@ public class LcpPublicationRetriever(
194195
}
195196
downloadsRepository.removeDownload(requestId.value)
196197

197-
val mediaTypeWithoutLicense = mediaTypeRetriever.retrieve(
198-
download.file,
199-
MediaTypeHints(
200-
mediaTypes = listOfNotNull(
201-
license.publicationLink.mediaType,
202-
download.mediaType
198+
val baseFormat =
199+
assetSniffer.sniff(
200+
download.file,
201+
FormatHints(
202+
mediaTypes = listOfNotNull(
203+
license.publicationLink.mediaType,
204+
download.mediaType
205+
)
203206
)
204-
)
205-
).getOrElse { MediaType.EPUB }
207+
).getOrElse { Format.EPUB }
208+
209+
val format = baseFormat + Trait.LCP_PROTECTED
206210

207211
try {
208212
// Saves the License Document into the downloaded publication
209-
val container = createLicenseContainer(download.file, mediaTypeWithoutLicense)
213+
val container = createLicenseContainer(download.file, format)
210214
container.write(license)
211215
} catch (e: Exception) {
212216
tryOrLog { download.file.delete() }
@@ -216,20 +220,10 @@ public class LcpPublicationRetriever(
216220
return@launch
217221
}
218222

219-
val mediaType = mediaTypeRetriever.retrieve(
220-
download.file,
221-
MediaTypeHints(
222-
mediaTypes = listOfNotNull(
223-
license.publicationLink.mediaType,
224-
download.mediaType
225-
)
226-
)
227-
).getOrElse { MediaType.EPUB }
228-
229223
val acquiredPublication = LcpService.AcquiredPublication(
230224
localFile = download.file,
231-
suggestedFilename = "${license.id}.${formatRegistry.fileExtension(mediaType) ?: "epub"}",
232-
mediaType = mediaType,
225+
suggestedFilename = "${license.id}.${format.fileExtension}",
226+
format,
233227
licenseDocument = license
234228
)
235229

@@ -285,4 +279,7 @@ public class LcpPublicationRetriever(
285279
listeners.remove(lcpRequestId)
286280
}
287281
}
282+
283+
private val Format.fileExtension: String get() =
284+
formatRegistry[this]?.fileExtension?.value ?: "epub"
288285
}

readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt

+15-16
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ import org.readium.r2.lcp.service.PassphrasesService
2929
import org.readium.r2.shared.publication.protection.ContentProtection
3030
import org.readium.r2.shared.util.Try
3131
import org.readium.r2.shared.util.asset.Asset
32-
import org.readium.r2.shared.util.asset.AssetRetriever
32+
import org.readium.r2.shared.util.asset.AssetOpener
33+
import org.readium.r2.shared.util.asset.AssetSniffer
3334
import org.readium.r2.shared.util.downloads.DownloadManager
34-
import org.readium.r2.shared.util.mediatype.MediaType
35-
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever
35+
import org.readium.r2.shared.util.format.Format
3636

3737
/**
3838
* Service used to acquire and open publications protected with LCP.
@@ -42,13 +42,12 @@ public interface LcpService {
4242
/**
4343
* Returns if the file is a LCP license document or a publication protected by LCP.
4444
*/
45+
@Deprecated(
46+
"Use an AssetSniffer and check the returned format for Trait.LCP_PROTECTED",
47+
level = DeprecationLevel.ERROR
48+
)
4549
public suspend fun isLcpProtected(file: File): Boolean
4650

47-
/**
48-
* Returns if the asset is a LCP license document or a publication protected by LCP.
49-
*/
50-
public suspend fun isLcpProtected(asset: Asset): Boolean
51-
5251
/**
5352
* Acquires a protected publication from a standalone LCPL's bytes.
5453
*
@@ -92,7 +91,7 @@ public interface LcpService {
9291
*/
9392
public suspend fun retrieveLicense(
9493
file: File,
95-
mediaType: MediaType,
94+
format: Format,
9695
authentication: LcpAuthenticating,
9796
allowUserInteraction: Boolean
9897
): Try<LcpLicense, LcpError>
@@ -146,7 +145,7 @@ public interface LcpService {
146145
public data class AcquiredPublication(
147146
val localFile: File,
148147
val suggestedFilename: String,
149-
val mediaType: MediaType,
148+
val format: Format,
150149
val licenseDocument: LicenseDocument
151150
) {
152151
@Deprecated(
@@ -164,8 +163,8 @@ public interface LcpService {
164163
*/
165164
public operator fun invoke(
166165
context: Context,
167-
assetRetriever: AssetRetriever,
168-
mediaTypeRetriever: MediaTypeRetriever,
166+
assetOpener: AssetOpener,
167+
assetSniffer: AssetSniffer,
169168
downloadManager: DownloadManager
170169
): LcpService? {
171170
if (!LcpClient.isAvailable()) {
@@ -176,7 +175,7 @@ public interface LcpService {
176175
val deviceRepository = DeviceRepository(db)
177176
val passphraseRepository = PassphrasesRepository(db)
178177
val licenseRepository = LicensesRepository(db)
179-
val network = NetworkService(mediaTypeRetriever)
178+
val network = NetworkService()
180179
val device = DeviceService(
181180
repository = deviceRepository,
182181
network = network,
@@ -191,8 +190,8 @@ public interface LcpService {
191190
network = network,
192191
passphrases = passphrases,
193192
context = context,
194-
assetRetriever = assetRetriever,
195-
mediaTypeRetriever = mediaTypeRetriever,
193+
assetOpener = assetOpener,
194+
assetSniffer = assetSniffer,
196195
downloadManager = downloadManager
197196
)
198197
}
@@ -203,7 +202,7 @@ public interface LcpService {
203202
ReplaceWith("LcpService(context, AssetRetriever(), MediaTypeRetriever())"),
204203
level = DeprecationLevel.ERROR
205204
)
206-
public fun create(context: Context): LcpService? = throw NotImplementedError()
205+
public fun create(context: Context): LcpService = throw NotImplementedError()
207206
}
208207

209208
@Deprecated(

0 commit comments

Comments
 (0)