diff --git a/xwiki-platform-core/pom.xml b/xwiki-platform-core/pom.xml
index 7a6071500d646ade1b9df9625f0cccaed9ebaacd..dd537a0743c2ffd20735fbb7656913a5884362bf 100644
--- a/xwiki-platform-core/pom.xml
+++ b/xwiki-platform-core/pom.xml
@@ -575,6 +575,7 @@
     <module>xwiki-platform-uiextension</module>
     <module>xwiki-platform-url</module>
     <module>xwiki-platform-velocity</module>
+    <module>xwiki-platform-vfs</module>
     <module>xwiki-platform-watchlist</module>
     <module>xwiki-platform-web</module>
     <module>xwiki-platform-webdav</module>
diff --git a/xwiki-platform-core/xwiki-platform-vfs/pom.xml b/xwiki-platform-core/xwiki-platform-vfs/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..17dafdcacb42d567db737e72478290b87f56e8ca
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.xwiki.platform</groupId>
+    <artifactId>xwiki-platform-core</artifactId>
+    <version>7.4-SNAPSHOT</version>
+  </parent>
+  <artifactId>xwiki-platform-vfs</artifactId>
+  <name>XWiki Platform - VFS API</name>
+  <description>VFS API</description>
+  <properties>
+    <xwiki.jacoco.instructionRatio>0.70</xwiki.jacoco.instructionRatio>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-resource-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-url-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-oldcore</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.commons</groupId>
+      <artifactId>xwiki-commons-script</artifactId>
+      <version>${commons.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>net.java.truevfs</groupId>
+      <artifactId>truevfs-profile-default</artifactId>
+      <version>0.11.0</version>
+      <type>pom</type>
+    </dependency>
+    <!-- Test Dependencies -->
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/AbstractContentResourceReferenceHandler.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/AbstractContentResourceReferenceHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0a15efac95b04d1cdc7cfc29b70246d662ebca3
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/AbstractContentResourceReferenceHandler.java
@@ -0,0 +1,73 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+
+import javax.inject.Inject;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.tika.Tika;
+import org.xwiki.container.Container;
+import org.xwiki.container.Response;
+import org.xwiki.resource.AbstractResourceReferenceHandler;
+import org.xwiki.resource.ResourceReferenceHandlerException;
+import org.xwiki.resource.ResourceType;
+
+/**
+ * Helper to implement {@link org.xwiki.resource.ResourceReferenceHandler} components that return content to the
+ * Container's output stream.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public abstract class AbstractContentResourceReferenceHandler extends AbstractResourceReferenceHandler<ResourceType>
+{
+    @Inject
+    private Container container;
+
+    /**
+     * Used to determine the Content Type of the requested resource files.
+     */
+    private Tika tika = new Tika();
+
+    protected void serveResource(String resourceName, InputStream resourceStream)
+        throws ResourceReferenceHandlerException
+    {
+        // Make sure the resource stream supports mark & reset which is needed in order be able to detect the
+        // content type without affecting the stream (Tika may need to read a few bytes from the start of the
+        // stream, in which case it will mark & reset the stream).
+        InputStream markResetSupportingStream = resourceStream;
+        if (!resourceStream.markSupported()) {
+            markResetSupportingStream = new BufferedInputStream(resourceStream);
+        }
+
+        try {
+            Response response = this.container.getResponse();
+            response.setContentType(this.tika.detect(markResetSupportingStream, resourceName));
+            IOUtils.copy(markResetSupportingStream, response.getOutputStream());
+        } catch (Exception e) {
+            throw new ResourceReferenceHandlerException(String.format("Failed to read resource [%s]", resourceName), e);
+        } finally {
+            IOUtils.closeQuietly(markResetSupportingStream);
+        }
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReference.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReference.java
new file mode 100644
index 0000000000000000000000000000000000000000..d6ed67327cbe230bd7c92c37a7fd4fa5cabd4dc3
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReference.java
@@ -0,0 +1,139 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.xwiki.resource.AbstractResourceReference;
+import org.xwiki.resource.ResourceType;
+import org.xwiki.text.XWikiToStringBuilder;
+
+/**
+ * Represents a reference to a VFS resource.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class VfsResourceReference extends AbstractResourceReference
+{
+    /**
+     * Represents a VFS Resource Type.
+     */
+    public static final ResourceType TYPE = new ResourceType("vfs");
+
+    private static final char RESOURCE_PATH_SEPARATOR = '/';
+
+    private URI uri;
+
+    private List<String> pathSegments;
+
+    /**
+     * @param uri the URI pointing to the archive (without the path inside the archive),
+     *       e.g. {@code attach:space.page@attachment}
+     * @param pathSegments see {@link #getPathSegments()}
+     */
+    public VfsResourceReference(URI uri, List<String> pathSegments)
+    {
+        setType(TYPE);
+        this.uri = uri;
+        this.pathSegments = new ArrayList<>(pathSegments);
+    }
+
+    /**
+     * @return the URI to the VFS (e.g. {@code attach:space.page@file.zip}, {@code http://server/path/to/zip})
+     */
+    public URI getURI()
+    {
+        return this.uri;
+    }
+
+    /**
+     * @return the list of segments pointing to the relative location of a resource in the VFS (e.g. {@code {"some",
+     * "directory", "file.txt"}} for {@code some/directory/file.txt}
+     */
+    public List<String> getPathSegments()
+    {
+        return this.pathSegments;
+    }
+
+    /**
+     * @return the String representation with "/" separating each VFS path segment, e.g. {@code some/directory/file.txt}
+     */
+    public String getPath()
+    {
+        return StringUtils.join(getPathSegments(), RESOURCE_PATH_SEPARATOR);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return new HashCodeBuilder(7, 7)
+            .append(getURI())
+            .append(getPathSegments())
+            .append(getType())
+            .append(getParameters())
+            .toHashCode();
+    }
+
+    @Override
+    public boolean equals(Object object)
+    {
+        if (object == null) {
+            return false;
+        }
+        if (object == this) {
+            return true;
+        }
+        if (object.getClass() != getClass()) {
+            return false;
+        }
+        VfsResourceReference rhs = (VfsResourceReference) object;
+        return new EqualsBuilder()
+            .append(getURI(), rhs.getURI())
+            .append(getPathSegments(), rhs.getPathSegments())
+            .append(getType(), rhs.getType())
+            .append(getParameters(), rhs.getParameters())
+            .isEquals();
+    }
+
+    @Override
+    public String toString()
+    {
+        ToStringBuilder builder = new XWikiToStringBuilder(this);
+        builder.append("uri", getURI());
+        builder.append("path", getPath());
+        builder.append("parameters", getParameters());
+        return builder.toString();
+    }
+
+    /**
+     * @return the resource reference as a URI
+     */
+    public URI toURI()
+    {
+        return URI.create(String.format("%s/%s", getURI().toString(), getPath()));
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceHandler.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fba7c26dd72593590c86b3464bc10ff2529838e
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceHandler.java
@@ -0,0 +1,136 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.component.manager.ComponentLookupException;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.component.phase.Initializable;
+import org.xwiki.component.phase.InitializationException;
+import org.xwiki.component.util.DefaultParameterizedType;
+import org.xwiki.resource.ResourceReference;
+import org.xwiki.resource.ResourceReferenceHandlerChain;
+import org.xwiki.resource.ResourceReferenceHandlerException;
+import org.xwiki.resource.ResourceReferenceSerializer;
+import org.xwiki.resource.ResourceType;
+import org.xwiki.resource.SerializeResourceReferenceException;
+import org.xwiki.resource.UnsupportedResourceReferenceException;
+import org.xwiki.vfs.internal.attach.AttachDriver;
+
+import net.java.truevfs.access.TArchiveDetector;
+import net.java.truevfs.access.TConfig;
+import net.java.truevfs.access.TPath;
+
+/**
+ * Handles VFS Resource References by outputting in the Container's response the resource pointed to in the archive
+ * file defined in the URL.
+ *
+ * @version $Id$
+ * @see VfsResourceReferenceResolver for the URL format handled
+ * @since 7.4M2
+ */
+@Component
+@Named("vfs")
+@Singleton
+public class VfsResourceReferenceHandler extends AbstractContentResourceReferenceHandler
+    implements Initializable
+{
+    @Inject
+    @Named("context")
+    private Provider<ComponentManager> componentManagerProvider;
+
+    @Override
+    public List<ResourceType> getSupportedResourceReferences()
+    {
+        return Arrays.asList(VfsResourceReference.TYPE);
+    }
+
+    @Override
+    public void initialize() throws InitializationException
+    {
+        // Register our Attach VFS Driver and inject a Component Manager in it.
+        TConfig config = TConfig.current();
+        // Note: Make sure we add our own Archive Detector to the existing Detector so that all archive formats
+        // supported by TrueVFS are handled properly.
+        config.setArchiveDetector(new TArchiveDetector(config.getArchiveDetector(), "attach",
+            new AttachDriver(this.componentManagerProvider.get())));
+    }
+
+    @Override
+    public void handle(ResourceReference resourceReference, ResourceReferenceHandlerChain chain)
+        throws ResourceReferenceHandlerException
+    {
+        // This code only handles VFS Resource References.
+        VfsResourceReference vfsResourceReference = (VfsResourceReference) resourceReference;
+
+        // Extract the asked resource from inside the zip and return its content for display.
+        try {
+            // We need to convert the VFS Resource Reference into a hierarchical URI supported by TrueVFS
+            URI trueVFSURI = convertResourceReference(vfsResourceReference);
+
+            // We use TrueVFS. This line will automatically use the VFS Driver that matches the scheme passed in the URI
+            Path path = new TPath(trueVFSURI);
+            try (InputStream in = Files.newInputStream(path)) {
+                List<String> pathSegments = vfsResourceReference.getPathSegments();
+                serveResource(pathSegments.get(pathSegments.size() - 1), in);
+            }
+        } catch (Exception e) {
+            throw new ResourceReferenceHandlerException(
+                String.format("Failed to extract resource [%s]", vfsResourceReference), e);
+        }
+
+        // Be a good citizen, continue the chain, in case some lower-priority Handler has something to do for this
+        // Resource Reference.
+        chain.handleNext(vfsResourceReference);
+    }
+
+    private URI convertResourceReference(VfsResourceReference reference) throws ResourceReferenceHandlerException
+    {
+        URI resultURI;
+
+        try {
+            ResourceReferenceSerializer<VfsResourceReference, URI> serializer =
+                this.componentManagerProvider.get().getInstance(new DefaultParameterizedType(null,
+                    ResourceReferenceSerializer.class, VfsResourceReference.class, URI.class),
+                        String.format("truevfs/%s", reference.getURI().getScheme()));
+            resultURI = serializer.serialize(reference);
+        } catch (ComponentLookupException e) {
+            // No serializer exist, we just don't perform any conversion!
+            resultURI = reference.toURI();
+        } catch (SerializeResourceReferenceException | UnsupportedResourceReferenceException e) {
+            throw new ResourceReferenceHandlerException(
+                String.format("Failed to convert VFS URI [%s] into a valid FS format", reference), e);
+        }
+
+        return resultURI;
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceResolver.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceResolver.java
new file mode 100644
index 0000000000000000000000000000000000000000..97158e554dee297ec36412c97193f2df9394b246
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceResolver.java
@@ -0,0 +1,84 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.reference.AttachmentReferenceResolver;
+import org.xwiki.resource.CreateResourceReferenceException;
+import org.xwiki.resource.ResourceType;
+import org.xwiki.resource.UnsupportedResourceReferenceException;
+import org.xwiki.url.ExtendedURL;
+import org.xwiki.url.internal.AbstractResourceReferenceResolver;
+
+/**
+ * Transform VFS URLs into a typed Resource Reference. The URL format handled is {@code http://server/<servlet
+ * context>/vfs/<vfs reference as URI>/path/inside/zip}. For example:
+ * <ul>
+ *   <li>{@code http://localhost:8080/xwiki/vfs/encoded(attach:space.page@attachment)/some/path/file.txt}.</li>
+ *   <li>{@code http://localhost:8080/xwiki/vfs/encoded(http://server/path/to/zip)/some/path/file.txt}.</li>
+ *   <li>{@code http://localhost:8080/xwiki/vfs/encoded(file://server/path/to/zip)/some/path/file.txt}.</li>
+ * </ul>
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+@Component
+@Named("vfs")
+@Singleton
+public class VfsResourceReferenceResolver extends AbstractResourceReferenceResolver
+{
+    @Inject
+    @Named("current")
+    private AttachmentReferenceResolver<String> referenceResolver;
+
+    @Override
+    public VfsResourceReference resolve(ExtendedURL extendedURL, ResourceType resourceType,
+        Map<String, Object> parameters) throws CreateResourceReferenceException, UnsupportedResourceReferenceException
+    {
+        List<String> segments = extendedURL.getSegments();
+
+        // First segment is the url-encoded VFS reference, defined as URI
+        URI vfsUri;
+        try {
+            vfsUri = new URI(segments.get(0));
+        } catch (URISyntaxException e) {
+            throw new CreateResourceReferenceException(
+                String.format("Invalid VFS URI [%s] for URL [%s]", segments.get(0), extendedURL));
+        }
+
+        // Other segments are the path to the archive resource
+        List<String> vfsPathSegments = new ArrayList<>(segments);
+        vfsPathSegments.remove(0);
+
+        VfsResourceReference vfsReference = new VfsResourceReference(vfsUri, vfsPathSegments);
+        copyParameters(extendedURL, vfsReference);
+        return vfsReference;
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceSerializer.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e30cea864a260b39f253dff89f65aed369b1b0a
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/VfsResourceReferenceSerializer.java
@@ -0,0 +1,72 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.resource.ResourceReferenceSerializer;
+import org.xwiki.resource.SerializeResourceReferenceException;
+import org.xwiki.resource.UnsupportedResourceReferenceException;
+import org.xwiki.url.ExtendedURL;
+import org.xwiki.url.URLNormalizer;
+
+/**
+ * Converts a {@link VfsResourceReference} into a relative {@link ExtendedURL} (with the Context Path added).
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+@Component
+@Singleton
+public class VfsResourceReferenceSerializer
+    implements ResourceReferenceSerializer<VfsResourceReference, ExtendedURL>
+{
+    @Inject
+    @Named("contextpath")
+    private URLNormalizer<ExtendedURL> extendedURLNormalizer;
+
+    @Override
+    public ExtendedURL serialize(VfsResourceReference resourceReference)
+        throws SerializeResourceReferenceException, UnsupportedResourceReferenceException
+    {
+        List<String> segments = new ArrayList<>();
+
+        // Add the resource type segment.
+        segments.add("vfs");
+
+        // Add the VFS URI part
+        segments.add(resourceReference.getURI().toString());
+
+        // Add the VFS path
+        segments.addAll(resourceReference.getPathSegments());
+
+        // Add all optional parameters
+        ExtendedURL extendedURL = new ExtendedURL(segments, resourceReference.getParameters());
+
+        // Normalize the URL to add the Context Path since we want a full relative URL to be returned.
+        return this.extendedURLNormalizer.normalize(extendedURL);
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachController.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachController.java
new file mode 100644
index 0000000000000000000000000000000000000000..215f55ae05651d0702bc5793a9cde9c91ac7fc45
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachController.java
@@ -0,0 +1,146 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal.attach;
+
+import java.io.IOException;
+
+import org.xwiki.component.manager.ComponentManager;
+
+import net.java.truecommons.cio.Entry;
+import net.java.truecommons.cio.Entry.Access;
+import net.java.truecommons.cio.Entry.Type;
+import net.java.truecommons.cio.InputSocket;
+import net.java.truecommons.cio.OutputSocket;
+import net.java.truecommons.shed.BitField;
+import net.java.truevfs.kernel.spec.FsAbstractController;
+import net.java.truevfs.kernel.spec.FsAccessOption;
+import net.java.truevfs.kernel.spec.FsController;
+import net.java.truevfs.kernel.spec.FsModel;
+import net.java.truevfs.kernel.spec.FsNodeName;
+import net.java.truevfs.kernel.spec.FsNodePath;
+import net.java.truevfs.kernel.spec.FsReadOnlyFileSystemException;
+import net.java.truevfs.kernel.spec.FsSyncOption;
+
+import static net.java.truecommons.cio.Entry.Access.READ;
+import static net.java.truecommons.cio.Entry.Type.FILE;
+
+/**
+ * TrueVFS Controller for the Attach driver.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class AttachController extends FsAbstractController
+{
+    private static final BitField<Access> READ_ONLY = BitField.of(READ);
+
+    private final AttachDriver driver;
+
+    private ComponentManager componentManager;
+
+    AttachController(AttachDriver driver, FsModel model, ComponentManager componentManager)
+    {
+        super(model);
+        this.driver = driver;
+        this.componentManager = componentManager;
+    }
+
+    private AttachNode newEntry(FsNodeName name)
+    {
+        return new AttachNode(this, name);
+    }
+
+    final FsNodePath resolve(FsNodeName name)
+    {
+        return getMountPoint().resolve(name);
+    }
+
+    @Override
+    public FsController getParent()
+    {
+        return null;
+    }
+
+    @Override
+    public AttachNode node(BitField<FsAccessOption> options, FsNodeName name) throws IOException
+    {
+        AttachNode entry = newEntry(name);
+        return entry.isType(FILE) ? entry : null;
+    }
+
+    @Override
+    public void checkAccess(BitField<FsAccessOption> options, FsNodeName name, BitField<Access> types)
+        throws IOException
+    {
+        // We only support READ at the moment
+        if (!types.isEmpty() && !READ_ONLY.equals(types)) {
+            throw new FsReadOnlyFileSystemException(getMountPoint());
+        }
+    }
+
+    @Override
+    public void setReadOnly(BitField<FsAccessOption> options, FsNodeName name) throws IOException
+    {
+        // All the Nodes (attachments) are already readonly, no need to set anything!
+    }
+
+    @Override
+    public boolean setTime(BitField<FsAccessOption> options, FsNodeName name, BitField<Access> types, long value)
+        throws IOException
+    {
+        throw new FsReadOnlyFileSystemException(getMountPoint());
+    }
+
+    @Override
+    public InputSocket<?> input(BitField<FsAccessOption> options, FsNodeName name)
+    {
+        return newEntry(name).input(options);
+    }
+
+    @Override
+    public OutputSocket<?> output(BitField<FsAccessOption> options, FsNodeName name, Entry template)
+    {
+        return newEntry(name).output(options, template);
+    }
+
+    @Override
+    public void make(final BitField<FsAccessOption> options, final FsNodeName name, Type type, Entry template)
+        throws IOException
+    {
+        throw new FsReadOnlyFileSystemException(getMountPoint());
+    }
+
+    @Override
+    public void unlink(BitField<FsAccessOption> options, FsNodeName name) throws IOException
+    {
+        throw new FsReadOnlyFileSystemException(getMountPoint());
+    }
+
+    @Override
+    public void sync(BitField<FsSyncOption> options)
+    {
+        // Empty
+    }
+
+    ComponentManager getComponentManager()
+    {
+        return this.componentManager;
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachDriver.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachDriver.java
new file mode 100644
index 0000000000000000000000000000000000000000..605c97d8ae11dbe8e9e7f12dd9995811e072f095
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachDriver.java
@@ -0,0 +1,53 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal.attach;
+
+import org.xwiki.component.manager.ComponentManager;
+
+import net.java.truevfs.kernel.spec.FsController;
+import net.java.truevfs.kernel.spec.FsDriver;
+import net.java.truevfs.kernel.spec.FsManager;
+import net.java.truevfs.kernel.spec.FsModel;
+
+/**
+ * TrueVFS Driver for archives attached to wiki pages as attachments.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class AttachDriver extends FsDriver
+{
+    private ComponentManager componentManager;
+
+    /**
+     * @param componentManager the Component Manager used to retrieve all other components since a TrueVFS driver is
+     *        not a Component and thus cannot have dependency-injection. Thus we need to pass the Component Manager.
+     */
+    public AttachDriver(ComponentManager componentManager)
+    {
+        this.componentManager = componentManager;
+    }
+
+    @Override
+    public FsController newController(FsManager manager, FsModel model, FsController parent)
+    {
+        return new AttachController(this, model, this.componentManager);
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachInputSocket.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachInputSocket.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6feb543de10911ef506b7d4b9129497860a2a7f
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachInputSocket.java
@@ -0,0 +1,100 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal.attach;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.SeekableByteChannel;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import net.java.truecommons.cio.AbstractInputSocket;
+import net.java.truecommons.cio.Entry;
+import net.java.truecommons.cio.IoBuffer;
+import net.java.truecommons.cio.IoSockets;
+import net.java.truecommons.cio.OutputSocket;
+import net.java.truecommons.io.ReadOnlyChannel;
+import net.java.truecommons.shed.BitField;
+import net.java.truevfs.kernel.spec.FsAccessOption;
+
+/**
+ * TrueVFS input socket for the Attach Driver.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+@NotThreadSafe
+public class AttachInputSocket extends AbstractInputSocket<AttachNode>
+{
+    private final AttachNode entry;
+
+    AttachInputSocket(BitField<FsAccessOption> options, AttachNode entry)
+    {
+        this.entry = entry;
+    }
+
+    @Override
+    public AttachNode target()
+    {
+        return entry;
+    }
+
+    @Override
+    public InputStream stream(final OutputSocket<? extends Entry> peer) throws IOException
+    {
+        return entry.newInputStream();
+    }
+
+    @Override
+    public SeekableByteChannel channel(final OutputSocket<? extends Entry> peer) throws IOException
+    {
+        final IoBuffer buffer = entry.getPool().allocate();
+        try {
+            IoSockets.copy(entry.input(), buffer.output());
+        } catch (final Throwable ex) {
+            try {
+                buffer.release();
+            } catch (final Throwable ex2) {
+                ex.addSuppressed(ex2);
+            }
+            throw ex;
+        }
+        final class BufferReadOnlyChannel extends ReadOnlyChannel
+        {
+            private boolean closed;
+
+            BufferReadOnlyChannel() throws IOException
+            {
+                super(buffer.input().channel(peer));
+            }
+
+            @Override
+            public void close() throws IOException
+            {
+                if (!closed) {
+                    channel.close();
+                    closed = true;
+                    buffer.release();
+                }
+            }
+        }
+        return new BufferReadOnlyChannel();
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachNode.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d674bbadbd1f20489f67910de5d708fd9a29296
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachNode.java
@@ -0,0 +1,192 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal.attach;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Set;
+
+import javax.annotation.concurrent.Immutable;
+
+import com.xpn.xwiki.doc.XWikiAttachment;
+
+import net.java.truecommons.cio.Entry;
+import net.java.truecommons.cio.InputSocket;
+import net.java.truecommons.cio.IoBufferPool;
+import net.java.truecommons.cio.IoEntry;
+import net.java.truecommons.cio.OutputSocket;
+import net.java.truecommons.shed.BitField;
+import net.java.truevfs.kernel.spec.FsAbstractNode;
+import net.java.truevfs.kernel.spec.FsAccessOption;
+import net.java.truevfs.kernel.spec.FsNodeName;
+import net.java.truevfs.kernel.spec.FsReadOnlyFileSystemException;
+import net.java.truevfs.kernel.spec.sl.IoBufferPoolLocator;
+
+import static net.java.truecommons.cio.Entry.Access.READ;
+import static net.java.truecommons.cio.Entry.Access.WRITE;
+import static net.java.truecommons.cio.Entry.Size.DATA;
+import static net.java.truecommons.cio.Entry.Type.FILE;
+import static net.java.truevfs.kernel.spec.FsAccessOptions.NONE;
+
+/**
+ * Represents a TrueVFS Node inside an archive located in an attachment in a wiki page.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+@Immutable
+public class AttachNode extends FsAbstractNode implements IoEntry<AttachNode>
+{
+    private final URI uri;
+
+    private final AttachController controller;
+
+    private final String name;
+
+    private XWikiModelNode xwikiModelNode;
+
+    AttachNode(final AttachController controller, final FsNodeName name)
+    {
+        assert null != controller;
+        this.controller = controller;
+        this.name = name.toString();
+        this.uri = controller.resolve(name).getUri();
+        this.xwikiModelNode = new XWikiModelNode(controller, name);
+    }
+
+    IoBufferPool getPool()
+    {
+        return IoBufferPoolLocator.SINGLETON.get();
+    }
+
+    protected InputStream newInputStream() throws IOException
+    {
+        // Return the attachment as an input stream
+        try {
+            return this.xwikiModelNode.getAttachment().getContentInputStream(this.xwikiModelNode.getXWikiContext());
+        } catch (Exception e) {
+            throw new IOException(String.format(
+                "Failed to get attachment content for attachment [%s] in URI [%s]", this.name, this.uri), e);
+        }
+    }
+
+    protected OutputStream newOutputStream() throws IOException
+    {
+        throw new FsReadOnlyFileSystemException(this.controller.getMountPoint());
+    }
+
+    @Override
+    public String getName()
+    {
+        return name;
+    }
+
+    @Override
+    public BitField<Type> getTypes()
+    {
+        // All Attach Driver Nodes are of type File, except if the Node doesn't exist, in which case we should return
+        // NO_TYPES
+        try {
+            return this.xwikiModelNode.getAttachment() != null ? FILE_TYPE : NO_TYPES;
+        } catch (IOException e) {
+            return NO_TYPES;
+        }
+    }
+
+    @Override
+    public boolean isType(final Type type)
+    {
+        return type == FILE && getTypes().is(FILE);
+    }
+
+    @Override
+    public long getSize(final Size type)
+    {
+        if (DATA != type) {
+            return UNKNOWN;
+        }
+        // Get the size of the attachment in bytes
+        try {
+            XWikiAttachment attachment = this.xwikiModelNode.getAttachment();
+            return attachment.getContentSize(this.xwikiModelNode.getXWikiContext());
+        } catch (Exception e) {
+            return UNKNOWN;
+        }
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public long getTime(Access type)
+    {
+        if (WRITE != type) {
+            return UNKNOWN;
+        }
+        // Get the last modified time for the attachment
+        try {
+            XWikiAttachment attachment = this.xwikiModelNode.getAttachment();
+            return attachment.getDate().getTime();
+        } catch (IOException e) {
+            return UNKNOWN;
+        }
+    }
+
+    @Override
+    public Boolean isPermitted(final Access type, final Entity entity)
+    {
+        if (READ != type) {
+            return null;
+        }
+        return true;
+    }
+
+    @Override
+    public Set<String> getMembers()
+    {
+        return null;
+    }
+
+    @Override
+    public final InputSocket<AttachNode> input()
+    {
+        return input(NONE);
+    }
+
+    /**
+     * @param options the options for accessing the file system node
+     * @return An input socket for reading this entry
+     */
+    protected InputSocket<AttachNode> input(BitField<FsAccessOption> options)
+    {
+        return new AttachInputSocket(options, this);
+    }
+
+    @Override
+    public final OutputSocket<AttachNode> output()
+    {
+        return output(NONE, null);
+    }
+
+    protected OutputSocket<AttachNode> output(BitField<FsAccessOption> options, Entry template)
+    {
+        return new AttachOutputSocket(options, this, template);
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachOutputSocket.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachOutputSocket.java
new file mode 100644
index 0000000000000000000000000000000000000000..26526fcde8019fdecec4af5a7b871a546d37586f
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/AttachOutputSocket.java
@@ -0,0 +1,60 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal.attach;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+import net.java.truecommons.cio.AbstractOutputSocket;
+import net.java.truecommons.cio.Entry;
+import net.java.truecommons.cio.InputSocket;
+import net.java.truecommons.shed.BitField;
+import net.java.truevfs.kernel.spec.FsAccessOption;
+
+/**
+ * TrueVFS output socket for the Attach Driver.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+@NotThreadSafe
+public class AttachOutputSocket extends AbstractOutputSocket<AttachNode>
+{
+    private final AttachNode entry;
+
+    AttachOutputSocket(BitField<FsAccessOption> options, AttachNode entry, Entry template)
+    {
+        this.entry = entry;
+    }
+
+    @Override
+    public AttachNode target()
+    {
+        return entry;
+    }
+
+    @Override
+    public OutputStream stream(final InputSocket<? extends Entry> peer) throws IOException
+    {
+        return entry.newOutputStream();
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/URIVfsResourceReferenceSerializer.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/URIVfsResourceReferenceSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..79c476f1261a1f9ceaea4d5305a9292e545a952a
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/URIVfsResourceReferenceSerializer.java
@@ -0,0 +1,72 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal.attach;
+
+import java.net.URI;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.reference.AttachmentReference;
+import org.xwiki.model.reference.AttachmentReferenceResolver;
+import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.resource.ResourceReferenceSerializer;
+import org.xwiki.resource.SerializeResourceReferenceException;
+import org.xwiki.resource.UnsupportedResourceReferenceException;
+import org.xwiki.vfs.internal.VfsResourceReference;
+
+/**
+ * Converts a {@link VfsResourceReference} into a {@link URI} in a format compatible with TrueVFS. Specifically TrueVFS
+ * requires a hierarchical URI. We make the following type of transformation:
+ * <ul>
+ *   <li>Example input: {@code attach:wiki:space.page@attachment/path/inside/archive}</li>
+ *   <li>Example output: {@code attach://wiki:space.page/attachment/path/inside/archive}</li>
+ * </ul>
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+@Component
+@Named("truevfs/attach")
+@Singleton
+public class URIVfsResourceReferenceSerializer implements ResourceReferenceSerializer<VfsResourceReference, URI>
+{
+    @Inject
+    @Named("current")
+    private AttachmentReferenceResolver<String> attachmentResolver;
+
+    @Inject
+    private EntityReferenceSerializer<String> documentSerializer;
+
+    @Override
+    public URI serialize(VfsResourceReference reference)
+        throws SerializeResourceReferenceException, UnsupportedResourceReferenceException
+    {
+        AttachmentReference attachmentReference =
+            this.attachmentResolver.resolve(reference.getURI().getSchemeSpecificPart());
+        String scheme = reference.getURI().getScheme();
+        String documentRefefenceString = this.documentSerializer.serialize(attachmentReference.getDocumentReference());
+
+        return URI.create(String.format("%s://%s/%s/%s", scheme, documentRefefenceString, attachmentReference.getName(),
+            reference.getPath()));
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/XWikiModelNode.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/XWikiModelNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..b553c350f2bed514fa74e5bc63077f15ab0594d5
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/internal/attach/XWikiModelNode.java
@@ -0,0 +1,154 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal.attach;
+
+import java.io.IOException;
+import java.net.URI;
+
+import javax.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.component.util.DefaultParameterizedType;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.DocumentReferenceResolver;
+import org.xwiki.security.authorization.ContextualAuthorizationManager;
+import org.xwiki.security.authorization.Right;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiAttachment;
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import net.java.truevfs.kernel.spec.FsNodeName;
+
+/**
+ * Decorator for an {@code AttachNode} to provide all XWiki Model API to get the Attachment corresponding to the VFS
+ * node.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class XWikiModelNode
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(XWikiModelNode.class);
+
+    private ComponentManager componentManager;
+
+    private DocumentReference reference;
+
+    private XWikiAttachment attachment;
+
+    private XWikiContext xcontext;
+
+    private URI uri;
+
+    private String name;
+
+    private ContextualAuthorizationManager authorizationManager;
+
+    XWikiModelNode(AttachController controller, FsNodeName name)
+    {
+        this.name = name.toString();
+        this.uri = controller.resolve(name).getUri();
+        this.componentManager = controller.getComponentManager();
+    }
+
+    private ComponentManager getComponentManager()
+    {
+        return this.componentManager;
+    }
+
+    /**
+     * @return the reference to the Document holding the archive attachment
+     * @throws IOException when an error accessing the Document occurs
+     */
+    public DocumentReference getDocumentReference() throws IOException
+    {
+        if (this.reference == null) {
+            try {
+                // Use a default resolver (and not a current one) since we don't have any context, we're in a new
+                // request.
+                DocumentReferenceResolver<String> documentReferenceResolver =
+                    getComponentManager().getInstance(DocumentReferenceResolver.TYPE_STRING);
+                this.reference = documentReferenceResolver.resolve(this.uri.getAuthority());
+            } catch (Exception e) {
+                throw new IOException(
+                    String.format("Failed to compute Document reference for [%s]", this.uri), e);
+            }
+        }
+        return this.reference;
+    }
+
+    /**
+     * @return the current XWiki Context
+     * @throws IOException if an error occurs retrieving the context
+     */
+    public XWikiContext getXWikiContext() throws IOException
+    {
+        if (this.xcontext == null) {
+            try {
+                Provider<XWikiContext> xcontextProvider = getComponentManager().getInstance(
+                    new DefaultParameterizedType(null, Provider.class, XWikiContext.class));
+                this.xcontext = xcontextProvider.get();
+            } catch (Exception e) {
+                throw new IOException(String.format("Failed to get XWiki Context for [%s]", this.uri), e);
+            }
+        }
+        return this.xcontext;
+    }
+
+    /**
+     * @return the archive attachment itself
+     * @throws IOException when an error accessing the Attachment occurs or if the current user doesn't have VIEW
+     *         permission on the Document to which it's attached to
+     */
+    public XWikiAttachment getAttachment() throws IOException
+    {
+        if (this.attachment == null) {
+            // Note that we check permission only once per Node for performance reason. As a consequence it's possible
+            // that a user who had permission at point A, and who's been denied it may still access the Node for some
+            // time.
+            checkViewPermission();
+            try {
+                XWikiDocument document = getXWikiContext().getWiki().getDocument(
+                    getDocumentReference(), getXWikiContext());
+                this.attachment = document.getAttachment(this.name);
+            } catch (Exception e) {
+                throw new IOException(String.format("Failed to get Attachment for [%s]", this.uri), e);
+            }
+        }
+        return this.attachment;
+    }
+
+    private void checkViewPermission() throws IOException
+    {
+        try {
+            if (this.authorizationManager == null) {
+                this.authorizationManager = getComponentManager().getInstance(ContextualAuthorizationManager.class);
+            }
+        } catch (Exception e) {
+            throw new IOException(String.format("Failed to check permission for [%s]", this.uri), e);
+        }
+        if (!this.authorizationManager.hasAccess(Right.VIEW, getDocumentReference())) {
+            throw new IOException(String.format("No View permission for document [%s]", getDocumentReference()));
+        }
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/script/VfsScriptService.java b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/script/VfsScriptService.java
new file mode 100644
index 0000000000000000000000000000000000000000..83e087c87e388613f5cdb85e4cca86df6192d176
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/java/org/xwiki/vfs/script/VfsScriptService.java
@@ -0,0 +1,70 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.script;
+
+import java.net.URI;
+import java.util.Arrays;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.commons.lang3.StringUtils;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.resource.ResourceReferenceSerializer;
+import org.xwiki.script.service.ScriptService;
+import org.xwiki.url.ExtendedURL;
+import org.xwiki.vfs.internal.VfsResourceReference;
+
+/**
+ * Offers scripting APIs for the VFS module.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+@Component
+@Named("vfs")
+@Singleton
+//@Unstable
+public class VfsScriptService implements ScriptService
+{
+    @Inject
+    private ResourceReferenceSerializer<VfsResourceReference, ExtendedURL> serializer;
+
+    /**
+     * Generate a VFS URL to access a resource inside an archive.
+     *
+     * @param resourceReference the string representation of a VFS resource reference which defines the location of an
+     *        archive. For example {@code attach:space.page@my.zip}.
+     * @param pathInArchive the path of the resource inside the archive for which to generate a URL for. For example
+     *        {@code /some/path/in/archive/test.txt}.
+     * @return a URL that can be used to access the content of a file inside an archive (ZIP, EAR, TAR.GZ, etc)
+     */
+    public String url(String resourceReference, String pathInArchive)
+    {
+        try {
+            VfsResourceReference vfsResourceReference = new VfsResourceReference(
+                new URI(resourceReference), Arrays.asList(StringUtils.split(pathInArchive, "/")));
+            return this.serializer.serialize(vfsResourceReference).toString();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-vfs/src/main/resources/META-INF/components.txt
new file mode 100644
index 0000000000000000000000000000000000000000..cfdb792343d452cd40d0e6d6e7480eafa6dfb12c
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/main/resources/META-INF/components.txt
@@ -0,0 +1,5 @@
+org.xwiki.vfs.internal.VfsResourceReferenceResolver
+org.xwiki.vfs.internal.VfsResourceReferenceSerializer
+org.xwiki.vfs.internal.VfsResourceReferenceHandler
+org.xwiki.vfs.script.VfsScriptService
+org.xwiki.vfs.internal.attach.URIVfsResourceReferenceSerializer
\ No newline at end of file
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceHandlerTest.java b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceHandlerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..64bb80d6cef540cda6c5fa6d4d325dbd17dec716
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceHandlerTest.java
@@ -0,0 +1,181 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.inject.Provider;
+
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.component.util.DefaultParameterizedType;
+import org.xwiki.container.Container;
+import org.xwiki.container.Response;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.DocumentReferenceResolver;
+import org.xwiki.resource.ResourceReferenceHandlerChain;
+import org.xwiki.resource.ResourceReferenceHandlerException;
+import org.xwiki.resource.ResourceReferenceSerializer;
+import org.xwiki.security.authorization.ContextualAuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.test.mockito.MockitoComponentMockingRule;
+
+import com.xpn.xwiki.XWiki;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiAttachment;
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for {@link VfsResourceReferenceHandler}.
+ * <p/>
+ * Note: We use a different URI for the various unit tests in this class since otherwise they're cached by
+ * TrueVFS. TODO: Find a way to flush TrueVFS caches.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class VfsResourceReferenceHandlerTest
+{
+    @Rule
+    public MockitoComponentMockingRule<VfsResourceReferenceHandler> mocker =
+        new MockitoComponentMockingRule<>(VfsResourceReferenceHandler.class);
+
+    private ByteArrayOutputStream baos;
+
+    private DocumentReference documentReference;
+
+    private VfsResourceReference reference;
+
+    private void setUp(String wikiName, String spaceName, String pageName, String attachmentName, List<String> path)
+        throws Exception
+    {
+        Provider<ComponentManager> componentManagerProvider = this.mocker.registerMockComponent(
+            new DefaultParameterizedType(null, Provider.class, ComponentManager.class), "context");
+        when(componentManagerProvider.get()).thenReturn(this.mocker);
+
+        String attachmentReferenceAsString =
+            String.format("attach:%s:%s.%s@%s", wikiName, spaceName, pageName, attachmentName);
+        this.reference = new VfsResourceReference(URI.create(attachmentReferenceAsString), path);
+
+        ResourceReferenceSerializer<VfsResourceReference, URI> serializer = mocker.registerMockComponent(
+            new DefaultParameterizedType(null, ResourceReferenceSerializer.class, VfsResourceReference.class,
+                URI.class), "truevfs/attach");
+        String truevfsURIFragment = String.format("attach://%s:%s.%s/%s/%s", wikiName, spaceName, pageName,
+            attachmentName, StringUtils.join(path, '/'));
+        when(serializer.serialize(this.reference)).thenReturn(URI.create(truevfsURIFragment));
+
+        Provider<XWikiContext> xwikiContextProvider = mocker.registerMockComponent(
+            new DefaultParameterizedType(null, Provider.class, XWikiContext.class));
+        XWikiContext xcontext = mock(XWikiContext.class);
+        when(xwikiContextProvider.get()).thenReturn(xcontext);
+
+        XWiki xwiki = mock(XWiki.class);
+        when(xcontext.getWiki()).thenReturn(xwiki);
+
+        DocumentReferenceResolver<String> documentReferenceResolver =
+            mocker.registerMockComponent(DocumentReferenceResolver.TYPE_STRING);
+        this.documentReference = new DocumentReference(wikiName, Arrays.asList(spaceName), pageName);
+        String documentReferenceAsString = String.format("%s:%s.%s", wikiName, spaceName, pageName);
+        when(documentReferenceResolver.resolve(documentReferenceAsString)).thenReturn(this.documentReference);
+
+        XWikiDocument document = mock(XWikiDocument.class);
+        when(xwiki.getDocument(this.documentReference, xcontext)).thenReturn(document);
+
+        XWikiAttachment attachment = mock(XWikiAttachment.class);
+        when(document.getAttachment(attachmentName)).thenReturn(attachment);
+
+        when(attachment.getDate()).thenReturn(new Date());
+        when(attachment.getContentSize(xcontext)).thenReturn(1000);
+
+        when(attachment.getContentInputStream(xcontext)).thenReturn(
+            createZipInputStream(StringUtils.join(path, '/'), "success!"));
+
+        Container container = this.mocker.getInstance(Container.class);
+        Response response = mock(Response.class);
+        when(container.getResponse()).thenReturn(response);
+
+        this.baos = new ByteArrayOutputStream();
+        when(response.getOutputStream()).thenReturn(this.baos);
+    }
+
+    @Test
+    public void handleWhenNotPermitted() throws Exception
+    {
+        setUp("wiki2", "space2", "page2", "test.zip", Arrays.asList("test.txt"));
+
+        // Disallow access to the document
+        ContextualAuthorizationManager authorizationManager =
+            this.mocker.registerMockComponent(ContextualAuthorizationManager.class);
+        when(authorizationManager.hasAccess(Right.VIEW, this.documentReference)).thenReturn(false);
+
+        try {
+            this.mocker.getComponentUnderTest().handle(this.reference, mock(ResourceReferenceHandlerChain.class));
+            fail("Should have raised an exception here");
+        } catch (ResourceReferenceHandlerException expected) {
+            assertEquals("Failed to extract resource [uri = [attach:wiki2:space2.page2@test.zip], path = [test.txt], "
+                + "parameters = []]", expected.getMessage());
+            // TODO: Find a better place to perform the permission check so that the error reported is better
+            assertEquals("NoSuchFileException: test.zip", ExceptionUtils.getRootCauseMessage(expected));
+        }
+    }
+
+    @Test
+    public void handleOk() throws Exception
+    {
+        setUp("wiki1", "space1", "page1", "test.zip", Arrays.asList("test.txt"));
+
+        // Allow access to the document
+        ContextualAuthorizationManager authorizationManager =
+            this.mocker.registerMockComponent(ContextualAuthorizationManager.class);
+        when(authorizationManager.hasAccess(Right.VIEW, this.documentReference)).thenReturn(true);
+
+        assertEquals(Arrays.asList(VfsResourceReference.TYPE),
+            this.mocker.getComponentUnderTest().getSupportedResourceReferences());
+        this.mocker.getComponentUnderTest().handle(this.reference, mock(ResourceReferenceHandlerChain.class));
+
+        assertEquals("success!", this.baos.toString());
+    }
+
+    private InputStream createZipInputStream(String fileName, String content) throws Exception
+    {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+            ZipEntry entry = new ZipEntry(fileName);
+            zos.putNextEntry(entry);
+            zos.write(content.getBytes());
+            zos.closeEntry();
+        }
+        return new ByteArrayInputStream(baos.toByteArray());
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceResolverTest.java b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceResolverTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..72d2b9ef1094b9584915169d913d01ce66255eaf
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceResolverTest.java
@@ -0,0 +1,60 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.xwiki.test.mockito.MockitoComponentMockingRule;
+import org.xwiki.url.ExtendedURL;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for {@link VfsResourceReferenceResolver}.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class VfsResourceReferenceResolverTest
+{
+    @Rule
+    public MockitoComponentMockingRule<VfsResourceReferenceResolver> mocker =
+        new MockitoComponentMockingRule<>(VfsResourceReferenceResolver.class);
+
+    @Test
+    public void resolve() throws Exception
+    {
+        ExtendedURL extendedURL = new ExtendedURL(
+            Arrays.asList("attach:wiki:space.page@attachment", "path1", "path2", "test.txt"),
+            Collections.singletonMap("key", Arrays.asList("value")));
+
+        VfsResourceReference reference = this.mocker.getComponentUnderTest().resolve(extendedURL,
+            VfsResourceReference.TYPE, Collections.<String, Object>emptyMap());
+
+        VfsResourceReference expected = new VfsResourceReference(URI.create("attach:wiki:space.page@attachment"),
+            Arrays.asList("path1", "path2", "test.txt"));
+        expected.addParameter("key", "value");
+        assertEquals(expected, reference);
+    }
+}
\ No newline at end of file
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceSerializerTest.java b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceSerializerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cb4be9b341fbac8a052210bb347dd9b58be520a
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceSerializerTest.java
@@ -0,0 +1,63 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.net.URI;
+import java.util.Arrays;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.xwiki.component.util.DefaultParameterizedType;
+import org.xwiki.test.mockito.MockitoComponentMockingRule;
+import org.xwiki.url.ExtendedURL;
+import org.xwiki.url.URLNormalizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link VfsResourceReferenceSerializer}.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class VfsResourceReferenceSerializerTest
+{
+    @Rule
+    public MockitoComponentMockingRule<VfsResourceReferenceSerializer> mocker =
+        new MockitoComponentMockingRule<>(VfsResourceReferenceSerializer.class);
+
+    @Test
+    public void serialize() throws Exception
+    {
+        VfsResourceReference reference = new VfsResourceReference(
+            URI.create("attach:wiki:space.page@attachment"), Arrays.asList("path1", "path2", "test.txt"));
+
+        ExtendedURL extendedURL = new ExtendedURL(Arrays.asList(
+            "vfs", "attach:wiki:space.page@attachment", "path1", "path2", "test.txt"));
+
+        URLNormalizer<ExtendedURL> normalizer = this.mocker.registerMockComponent(
+            new DefaultParameterizedType(null, URLNormalizer.class, ExtendedURL.class), "contextpath");
+        when(normalizer.normalize(extendedURL)).thenReturn(extendedURL);
+
+        assertEquals("/vfs/attach%3Awiki%3Aspace.page%40attachment/path1/path2/test.txt",
+            this.mocker.getComponentUnderTest().serialize(reference).toString());
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceTest.java b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a15690e9a906791968123ddd4d9ef77f19366eff
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/internal/VfsResourceReferenceTest.java
@@ -0,0 +1,71 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.internal;
+
+import java.net.URI;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for {@link VfsResourceReference}.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class VfsResourceReferenceTest
+{
+    @Test
+    public void equality()
+    {
+        VfsResourceReference reference1 =
+            new VfsResourceReference(URI.create("scheme:specific"), Arrays.asList("a", "b"));
+        reference1.addParameter("key", "value");
+        VfsResourceReference reference2 =
+            new VfsResourceReference(URI.create("scheme:specific"), Arrays.asList("a", "b"));
+
+        assertNotEquals(reference1, reference2);
+
+        reference2.addParameter("key", "value");
+        assertEquals(reference1, reference2);
+        assertEquals(reference1.hashCode(), reference2.hashCode());
+    }
+
+    @Test
+    public void toURI()
+    {
+        VfsResourceReference reference =
+            new VfsResourceReference(URI.create("scheme:specific"), Arrays.asList("a", "b"));
+
+        URI expected = URI.create("scheme:specific/a/b");
+        assertEquals(expected, reference.toURI());
+    }
+
+    @Test
+    public void stringValue()
+    {
+        VfsResourceReference reference =
+            new VfsResourceReference(URI.create("scheme:specific"), Arrays.asList("a", "b"));
+        reference.addParameter("key", "value");
+        assertEquals("uri = [scheme:specific], path = [a/b], parameters = [[key] = [[value]]]", reference.toString());
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/script/VfsScriptServiceTest.java b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/script/VfsScriptServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6232e410987f31dcbd81c4f48a97b3ac8dbadfa3
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-vfs/src/test/java/org/xwiki/vfs/script/VfsScriptServiceTest.java
@@ -0,0 +1,62 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.vfs.script;
+
+import java.net.URI;
+import java.util.Arrays;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.xwiki.component.util.DefaultParameterizedType;
+import org.xwiki.resource.ResourceReferenceSerializer;
+import org.xwiki.test.mockito.MockitoComponentMockingRule;
+import org.xwiki.url.ExtendedURL;
+import org.xwiki.vfs.internal.VfsResourceReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link VfsScriptService}.
+ *
+ * @version $Id$
+ * @since 7.4M2
+ */
+public class VfsScriptServiceTest
+{
+    @Rule
+    public MockitoComponentMockingRule<VfsScriptService> mocker =
+        new MockitoComponentMockingRule<>(VfsScriptService.class);
+
+    @Test
+    public void url() throws Exception
+    {
+        VfsResourceReference reference = new VfsResourceReference(
+            URI.create("attach:xwiki:space.page@attachment"), Arrays.asList("path1", "path2", "test.txt"));
+
+        ResourceReferenceSerializer<VfsResourceReference, ExtendedURL> serializer = this.mocker.getInstance(
+            new DefaultParameterizedType(null, ResourceReferenceSerializer.class, VfsResourceReference.class,
+                ExtendedURL.class));
+        when(serializer.serialize(reference)).thenReturn(new ExtendedURL(Arrays.asList("generated", "url")));
+
+        assertEquals("/generated/url",
+            this.mocker.getComponentUnderTest().url("attach:xwiki:space.page@attachment", "path1/path2/test.txt"));
+    }
+}