001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.common.io.hashbrown; 019 020import com.google.common.io.ByteSource; 021import com.google.common.io.CharSource; 022 023import dev.enola.common.io.iri.URIs; 024import dev.enola.common.io.resource.*; 025 026import java.io.IOException; 027import java.io.UncheckedIOException; 028import java.net.URI; 029import java.util.concurrent.atomic.AtomicBoolean; 030import java.util.concurrent.locks.Lock; 031import java.util.concurrent.locks.ReentrantLock; 032 033public class IntegrityValidatingDelegatingResource extends DelegatingResource { 034 035 // TODO This needs to improved to re-hash() when Resource.version() [content] changes! 036 // (For both scenarios; when it was valid, and when it was not.) 037 038 public static class Provider implements ResourceProvider { 039 private final ResourceProvider delegatingResourceProvider; 040 041 public Provider(ResourceProvider delegatingResourceProvider) { 042 this.delegatingResourceProvider = delegatingResourceProvider; 043 } 044 045 @Override 046 public Resource getResource(URI uri) { 047 var original = delegatingResourceProvider.getResource(uri); 048 if (original == null) return null; 049 var integrity = URIs.getQueryMap(uri).get("integrity"); 050 if (integrity == null) return original; 051 var multihash = MultihashWithMultibase.decode(integrity); 052 return new IntegrityValidatingDelegatingResource(original, multihash); 053 } 054 } 055 056 private final MultihashWithMultibase expectedHash; 057 private final AtomicBoolean validated = new AtomicBoolean(false); 058 private final Lock validationLock = new ReentrantLock(); 059 060 public IntegrityValidatingDelegatingResource( 061 Resource delegate, MultihashWithMultibase expectedHash) { 062 super(delegate); 063 this.expectedHash = expectedHash; 064 } 065 066 @Override 067 public ByteSource byteSource() { 068 ensureValidated(); 069 return delegate.byteSource(); 070 } 071 072 @Override 073 public CharSource charSource() { 074 ensureValidated(); 075 return delegate.charSource(); 076 } 077 078 private void ensureValidated() { 079 if (validated.get()) { 080 return; 081 } 082 083 validationLock.lock(); 084 try { 085 if (!validated.get()) { 086 validate(); 087 validated.set(true); 088 } 089 } finally { 090 validationLock.unlock(); 091 } 092 } 093 094 private void validate() { 095 try { 096 var resourceHasher = new ResourceHasher(); 097 var actualHash = resourceHasher.hash(delegate, expectedHash.multihash().getType()); 098 if (!expectedHash.multihash().equals(actualHash)) { 099 throw new IntegrityViolationException( 100 "Expected " 101 + expectedHash 102 + " but got " 103 + Multihashes.toString(actualHash, expectedHash.multibase())); 104 } 105 } catch (IOException e) { 106 throw new UncheckedIOException(e); 107 } 108 } 109}