Skip to content

Commit

Permalink
feat (cli): Add multibase: resource scheme URL fetch support
Browse files Browse the repository at this point in the history
  • Loading branch information
vorburger committed Mar 1, 2025
1 parent 6354265 commit 3bad4c8
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 12 deletions.
9 changes: 9 additions & 0 deletions docs/use/fetch/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ $ ./enola fetch "data:application/json;charset=UTF-8,%7B%22key%22%3A+%22value%22
...
```

### Multibase

Enola supports `multibase:` URLs, which are from [Multiformat](https://multiformats.io/):

```bash cd ../.././..
$ ./enola fetch multibase:maGVsbG8sIHdvcmxk
...
```

### File Descriptor

`fd:` is a (non-standard) URL scheme in Enola for reading from or writing to [file descriptors](https://en.wikipedia.org/wiki/File_descriptor), for:
Expand Down
1 change: 1 addition & 0 deletions java/dev/enola/cli/CommandWithResourceProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public Integer call() throws Exception {
protected void run() throws Exception {
var builder = ImmutableList.<ResourceProvider>builder();
builder.add(new DataResource.Provider());
builder.add(new MultibaseResource.Provider());
builder.add(new EmptyResource.Provider());
builder.add(new StringResource.Provider());
if (file) {
Expand Down
1 change: 1 addition & 0 deletions java/dev/enola/common/io/mediatype/MediaTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static MediaType parse(String input) {

@Deprecated // Remove and replace (inline) with implementation, for clarity
public static MediaType normalize(MediaType mediaType) {
if (MediaType.OCTET_STREAM.equals(mediaType)) return mediaType;
return MediaTypeProviders.SINGLETON.get().normalize(mediaType);
}

Expand Down
5 changes: 5 additions & 0 deletions java/dev/enola/common/io/resource/DataResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
/**
* Resource I/O implementation for RFC 2397 <a
* href="https://en.m.wikipedia.org/wiki/Data_URI_scheme">data: URLs</a>.
*
* <p>@see {@link MultibaseResource} for another similar URL scheme.
*/
public class DataResource extends BaseResource implements ReadableButNotWritableResource {

Expand Down Expand Up @@ -102,6 +104,9 @@ private static byte[] extractBytes(URI uri) {
.getBytes(DATA_DEFAULT_CHARSET);
}

// TODO public static Resource of(ByteSource byteSource, MediaType mediaType) {
// java.util.Base64.getEncoder().withoutPadding().encode(byteSource.read());

public static Resource of(@Nullable String text, @Nullable MediaType mediaType) {
var data = MediaTypes.toStringWithoutSpaces(mediaType) + "," + Strings.nullToEmpty(text);
try {
Expand Down
4 changes: 3 additions & 1 deletion java/dev/enola/common/io/resource/MediaTypeDetector.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ private MediaType detectCharsetAndMediaType(
}
}
if (!isSpecial(detected)) return detected;
else return MediaTypeProviders.SINGLETON.get().detect(uri.toString(), byteSource, detected);
var uriString = uri.toString();
if (!uriString.contains(".") && byteSource.equals(ByteSource.empty())) return detected;
return MediaTypeProviders.SINGLETON.get().detect(uri.toString(), byteSource, detected);
}

private MediaType fixMissingCharset(MediaType mediaType) {
Expand Down
85 changes: 85 additions & 0 deletions java/dev/enola/common/io/resource/MultibaseResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024-2025 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.common.io.resource;

import com.google.common.base.Strings;
import com.google.common.io.ByteSource;
import com.google.common.net.MediaType;

import dev.enola.common.io.iri.URIs;
import dev.enola.common.io.mediatype.MediaTypes;

import io.ipfs.multibase.Multibase;

import org.jspecify.annotations.Nullable;

import java.net.URI;
import java.net.URISyntaxException;

/**
* Resource I/O implementation based on a <a
* href="https://github.com/multiformats/multibase">multibase: URLs</a> (invented by Enola.dev; not
* standardized anywhere, yet).
*
* <p>@see {@link DataResource} for another similar URL scheme.
*/
public class MultibaseResource extends BaseResource implements ReadableButNotWritableResource {

private static final String SCHEME = "multibase";

public static class Provider implements ResourceProvider {
@Override
public Resource getResource(URI uri) {
if (SCHEME.equals(uri.getScheme())) return new MultibaseResource(uri);
else return null;
}
}

private final ByteSource byteSource;

public static Resource of(@Nullable String text, @Nullable MediaType mediaType) {
var data = MediaTypes.toStringWithoutSpaces(mediaType) + "," + Strings.nullToEmpty(text);
try {
return new MultibaseResource(new URI(SCHEME, data, null));
} catch (URISyntaxException e) {
throw new IllegalArgumentException(data, e);
}
}

public static Resource of(@Nullable String text) {
return of(text, null);
}

// TODO Rewrite using https://blog.jetbrains.com/idea/2024/02/constructor-makeover-in-java-22/
// once we're on Java >21 (and avoid having to match the RegExp twice, which is inefficient)
private static URI checkSchema(URI uri) {
if (!SCHEME.equals(uri.getScheme())) throw new IllegalArgumentException(uri.toString());
return uri;
}

public MultibaseResource(URI uri) {
super(checkSchema(uri));
String data = URIs.dropQueryAndFragment(uri).getSchemeSpecificPart();
byteSource = ByteSource.wrap(Multibase.decode(data));
}

@Override
public ByteSource byteSource() {
return byteSource;
}
}
44 changes: 44 additions & 0 deletions java/dev/enola/common/io/resource/MultibaseResourceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024-2025 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.common.io.resource;

import static com.google.common.truth.Truth.assertThat;

import org.junit.Test;

import java.io.IOException;
import java.net.URI;

public class MultibaseResourceTest {

@Test
public void hex() throws IOException {
assertThat(new MultibaseResource(URI.create("multibase:f0a3f")).byteSource().read())
.isEqualTo(new byte[] {0x0a, 0x3f});
}

@Test(expected = IllegalArgumentException.class)
public void spaceIsInvalid() {
new DataResource(URI.create("multibase:f0 a3f"));
}

@Test(expected = IllegalArgumentException.class)
public void empty() {
new MultibaseResource(URI.create(""));
}
}
12 changes: 6 additions & 6 deletions java/dev/enola/common/io/resource/ResourceProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public interface ResourceProvider extends ProviderFromIRI<Resource> {
// TODO Should this have a Resource getResource(URI uri, MediaType mediaType) ?

@Override
default @Nullable Resource get(String iri) {
return getResource(URI.create(iri));
default @Nullable Resource get(String uri) {
return getResource(URI.create(uri));
}

// TODO Rename getResource to get for consistency
Expand All @@ -54,17 +54,17 @@ public interface ResourceProvider extends ProviderFromIRI<Resource> {
return getResource(uri);
}

default @Nullable ReadableResource getReadableResource(String iri) {
return get(iri);
default @Nullable ReadableResource getReadableResource(String uri) {
return get(uri);
}

// TODO getWritableResource() not @Nullable, but throws UnregisteredURISchemeException
default @Nullable WritableResource getWritableResource(URI uri) {
return getResource(uri);
}

default @Nullable WritableResource getWritableResource(String iri) {
return get(iri);
default @Nullable WritableResource getWritableResource(String uri) {
return get(uri);
}

// -------------------------------------------
Expand Down
6 changes: 2 additions & 4 deletions java/dev/enola/common/io/resource/StringResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@
/**
* Non-standard Enola specific "string:hello" Resource I/O implementation.
*
* <p>@deprecated Use {@link DataResource} instead of this!
* <p>@deprecated Use {@link DataResource} (or {@link MultibaseResource}) instead of this!
*/
@Deprecated // TODO Replace all original StringResource usages with DataResource, and remove
// TODO Cannot be replaced, because data: cannot have #fragment - how about MultibaseResource?
public class StringResource extends BaseResource implements ReadableButNotWritableResource {
// TODO Replace ReadableButNotWritableResource with ReadableResource #again

public static class Provider implements ResourceProvider {

@Override
public Resource getResource(URI uri) {
if (SCHEME.equals(uri.getScheme()))
Expand All @@ -55,7 +54,6 @@ public Resource getResource(URI uri) {

private final String string;

// TODO Hm, this actually cannot be replaced with DataResource (data: cannot have #fragment)
public static Resource of(@Nullable String text, MediaType mediaType, URI fragmentURI) {
if (Strings.isNullOrEmpty(text)) {
return new EmptyResource(fragmentURI, mediaType);
Expand Down
2 changes: 1 addition & 1 deletion models/enola.dev/binary.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

@prefix enola: <https://enola.dev/>.
@prefix java: <https://enola.dev/java/>.
@prefix proto: <https://enola.dev/proto/>.
@prefix proto: <https://enola.dev/proto/>.
@prefix mf: <https://multiformats.io/>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
Expand Down

0 comments on commit 3bad4c8

Please sign in to comment.