diff --git a/apiary.apib b/apiary.apib index b420f8b0ae1..e60e52702f5 100644 --- a/apiary.apib +++ b/apiary.apib @@ -145,6 +145,90 @@ The `Content-type` header of the reply will be set accordingly. genre as identified by analyzer, could be PLAIN, XREFABLE, IMAGE, DATA, HTML +## File definitions [/file/defs{?path}] + +### get file definitions [GET] + ++ Parameters + + path (string) - path of file, relative to source root + ++ Response 200 (application/json) + + Body + + [ + { + "type": "function", + "signature": "(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "text": "void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,", + "symbol": "AES_cbc_encrypt", + "lineStart": 5, + "lineEnd": 20, + "line": 20, + "namespace": null + }, + { + "type": "argument", + "signature": "(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "text": "AES_cbc_encrypt(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "symbol": "in", + "lineStart": 21, + "lineEnd": 44, + "line": 20, + "namespace": null + }, + { + "type": "argument", + "signature": "(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "text": "AES_cbc_encrypt(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "symbol": "out", + "lineStart": 46, + "lineEnd": 64, + "line": 20, + "namespace": null + }, + { + "type": "argument", + "signature": "(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "text": "AES_cbc_encrypt(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "symbol": "len", + "lineStart": 21, + "lineEnd": 31, + "line": 21, + "namespace": null + }, + { + "type": "argument", + "signature": "(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "text": "AES_cbc_encrypt(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "symbol": "key", + "lineStart": 33, + "lineEnd": 51, + "line": 21, + "namespace": null + }, + { + "type": "argument", + "signature": "(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "text": "AES_cbc_encrypt(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "symbol": "ivec", + "lineStart": 21, + "lineEnd": 40, + "line": 22, + "namespace": null + }, + { + "type": "argument", + "signature": "(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "text": "AES_cbc_encrypt(const unsigned char * in,unsigned char * out,size_t len,const AES_KEY * key,unsigned char * ivec,const int enc)", + "symbol": "enc", + "lineStart": 42, + "lineEnd": 55, + "line": 22, + "namespace": null + } + ] + + ## History [/history{?path,withFiles,start,max}] ### get history entries [GET] diff --git a/opengrok-indexer/src/main/java/org/opengrok/indexer/analysis/Definitions.java b/opengrok-indexer/src/main/java/org/opengrok/indexer/analysis/Definitions.java index 08b0cdc81d3..6c26ecb04a0 100644 --- a/opengrok-indexer/src/main/java/org/opengrok/indexer/analysis/Definitions.java +++ b/opengrok-indexer/src/main/java/org/opengrok/indexer/analysis/Definitions.java @@ -18,12 +18,13 @@ */ /* - * Copyright (c) 2008, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved. * Portions Copyright (c) 2018, Chris Fraire . */ package org.opengrok.indexer.analysis; import org.jetbrains.annotations.Nullable; +import org.opengrok.indexer.util.DTOElement; import org.opengrok.indexer.util.WhitelistObjectInputFilter; import java.io.ByteArrayInputStream; @@ -206,39 +207,80 @@ public static class Tag implements Serializable { private static final long serialVersionUID = 1217869075425651465L; + public int getLine() { + return line; + } + + public String getSymbol() { + return symbol; + } + /** * Line number of the tag. */ + @DTOElement public final int line; /** * The symbol used in the definition. */ + @DTOElement public final String symbol; + + public String getType() { + return type; + } + + public String getText() { + return text; + } + + public String getNamespace() { + return namespace; + } + + public String getSignature() { + return signature; + } + + public int getLineStart() { + return lineStart; + } + + public int getLineEnd() { + return lineEnd; + } + /** * The type of the tag. */ + @DTOElement public final String type; /** * The full line on which the definition occurs. */ + @DTOElement public final String text; /** * Namespace/class of tag definition. */ + @DTOElement public final String namespace; /** * Scope of tag definition. */ + @DTOElement public final String signature; /** * The starting offset (possibly approximate) of {@link #symbol} from * the start of the line. */ + @DTOElement public final int lineStart; /** * The ending offset (possibly approximate) of {@link #symbol} from * the start of the line. */ + @DTOElement public final int lineEnd; /** @@ -246,6 +288,10 @@ public static class Tag implements Serializable { */ private transient boolean used; + protected Tag() { + this(0, null, null, null, null, null, 0, 0); + } + protected Tag(int line, String symbol, String type, String text, String namespace, String signature, int lineStart, int lineEnd) { diff --git a/opengrok-web/src/main/java/org/opengrok/web/api/v1/controller/FileController.java b/opengrok-web/src/main/java/org/opengrok/web/api/v1/controller/FileController.java index 149239cd1c2..981d24fc844 100644 --- a/opengrok-web/src/main/java/org/opengrok/web/api/v1/controller/FileController.java +++ b/opengrok-web/src/main/java/org/opengrok/web/api/v1/controller/FileController.java @@ -18,7 +18,7 @@ */ /* - * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved. * Portions Copyright (c) 2020, Chris Fraire . */ package org.opengrok.web.api.v1.controller; @@ -35,9 +35,12 @@ import org.apache.lucene.document.Document; import org.apache.lucene.queryparser.classic.ParseException; import org.opengrok.indexer.analysis.AbstractAnalyzer; +import org.opengrok.indexer.analysis.Definitions; +import org.opengrok.indexer.index.IndexDatabase; import org.opengrok.indexer.search.QueryBuilder; import org.opengrok.web.api.v1.filter.CorsEnable; import org.opengrok.web.api.v1.filter.PathAuthorized; +import org.opengrok.web.util.DTOUtil; import org.opengrok.web.util.NoPathParameterException; import java.io.File; @@ -45,6 +48,10 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; import static org.opengrok.indexer.index.IndexDatabase.getDocument; import static org.opengrok.web.util.FileUtil.toFile; @@ -54,6 +61,8 @@ public class FileController { public static final String PATH = "file"; + + private StreamingOutput transfer(File file) throws FileNotFoundException { if (!file.exists()) { throw new FileNotFoundException(String.format("file %s does not exist", file)); @@ -141,4 +150,24 @@ public String getGenre(@Context HttpServletRequest request, return genre.toString(); } + + @GET + @CorsEnable + @PathAuthorized + @Path("/defs") + @Produces(MediaType.APPLICATION_JSON) + public List getDefinitions(@Context HttpServletRequest request, + @Context HttpServletResponse response, + @QueryParam("path") final String path) + throws IOException, NoPathParameterException, ParseException, ClassNotFoundException { + + File file = toFile(path); + Definitions defs = IndexDatabase.getDefinitions(file); + return Optional.ofNullable(defs). + map(Definitions::getTags). + stream(). + flatMap(Collection::stream). + map(DTOUtil::createDTO). + collect(Collectors.toList()); + } } diff --git a/opengrok-web/src/test/java/org/opengrok/web/api/v1/controller/FileControllerTest.java b/opengrok-web/src/test/java/org/opengrok/web/api/v1/controller/FileControllerTest.java index dee478a89e7..34ca34bb267 100644 --- a/opengrok-web/src/test/java/org/opengrok/web/api/v1/controller/FileControllerTest.java +++ b/opengrok-web/src/test/java/org/opengrok/web/api/v1/controller/FileControllerTest.java @@ -18,42 +18,73 @@ */ /* - * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved. * Portions Copyright (c) 2020, Chris Fraire . */ package org.opengrok.web.api.v1.controller; -import jakarta.ws.rs.core.Application; -import org.glassfish.jersey.server.ResourceConfig; +import jakarta.ws.rs.core.GenericType; +import jakarta.ws.rs.core.Response; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerException; +import org.glassfish.jersey.test.spi.TestContainerFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.opengrok.indexer.analysis.Definitions; +import org.opengrok.indexer.authorization.AuthControlFlag; +import org.opengrok.indexer.authorization.AuthorizationFramework; +import org.opengrok.indexer.authorization.AuthorizationPlugin; +import org.opengrok.indexer.authorization.AuthorizationStack; import org.opengrok.indexer.configuration.RuntimeEnvironment; import org.opengrok.indexer.history.HistoryGuru; import org.opengrok.indexer.history.RepositoryFactory; +import org.opengrok.indexer.index.IndexDatabase; import org.opengrok.indexer.index.Indexer; import org.opengrok.indexer.util.TestRepository; +import org.opengrok.web.api.v1.RestApp; +import java.io.File; import java.io.IOException; +import java.net.URL; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class FileControllerTest extends OGKJerseyTest { private final RuntimeEnvironment env = RuntimeEnvironment.getInstance(); + private static final String validPath = "git/main.c"; + private TestRepository repository; @Override - protected Application configure() { - return new ResourceConfig(FileController.class); + protected DeploymentContext configureDeployment() { + return ServletDeploymentContext.forServlet(new ServletContainer(new RestApp())).build(); } + @Override + protected TestContainerFactory getTestContainerFactory() throws TestContainerException { + return new GrizzlyWebTestContainerFactory(); + } + + @BeforeEach @Override public void setUp() throws Exception { @@ -105,12 +136,80 @@ void testFileContent() throws IOException { @Test void testFileGenre() { - final String path = "git/main.c"; String genre = target("file") .path("genre") - .queryParam("path", path) + .queryParam("path", validPath) .request() .get(String.class); assertEquals("PLAIN", genre); } + + @Test + void testFileDefinitions() { + GenericType> type = new GenericType<>() { + }; + List defs = target("file") + .path("defs") + .queryParam("path", validPath) + .request() + .get(type); + assertFalse(defs.isEmpty()); + assertAll(() -> assertFalse(defs.stream().map(Definitions.Tag::getType).anyMatch(Objects::isNull)), + () -> assertFalse(defs.stream().map(Definitions.Tag::getSymbol).anyMatch(Objects::isNull)), + () -> assertFalse(defs.stream().map(Definitions.Tag::getText).anyMatch(Objects::isNull)), + () -> assertFalse(defs.stream().filter(e -> !e.getType().equals("local")). + map(Definitions.Tag::getSignature).anyMatch(Objects::isNull)), + () -> assertFalse(defs.stream().map(Definitions.Tag::getLine).anyMatch(e -> e <= 0)), + () -> assertFalse(defs.stream().map(Definitions.Tag::getLineStart).anyMatch(e -> e <= 0)), + () -> assertFalse(defs.stream().map(Definitions.Tag::getLineEnd).anyMatch(e -> e <= 0))); + } + + /** + * Negative test case for file definitions API endpoint for a file that exists under the source root, + * however does not have matching document in the respective index database. + */ + @Test + void testFileDefinitionsNull() throws Exception { + final String path = "git/extra.file"; + Path createdPath = Files.createFile(Path.of(env.getSourceRootPath(), path)); + // Assumes that the API endpoint indeed calls IndexDatabase#getDefinitions() + assertNull(IndexDatabase.getDefinitions(createdPath.toFile())); + GenericType> type = new GenericType<>() { + }; + List defs = target("file") + .path("defs") + .queryParam("path", path) + .request() + .get(type); + assertTrue(defs.isEmpty()); + } + + /** + * Make sure that file definitions API performs authorization check. + */ + @Test + void testFileDefinitionsNotAuthorized() throws Exception { + AuthorizationStack stack = new AuthorizationStack(AuthControlFlag.REQUIRED, "stack"); + stack.add(new AuthorizationPlugin(AuthControlFlag.REQUIRED, "opengrok.auth.plugin.FalsePlugin")); + URL pluginsURL = getClass().getResource("/testplugins.jar"); + assertNotNull(pluginsURL); + Path pluginsPath = Paths.get(pluginsURL.toURI()); + assertNotNull(pluginsPath); + File pluginDirectory = pluginsPath.toFile().getParentFile(); + assertNotNull(pluginDirectory); + assertTrue(pluginDirectory.isDirectory()); + AuthorizationFramework framework = new AuthorizationFramework(pluginDirectory.getPath(), stack); + framework.setLoadClasses(false); // to avoid noise when loading classes of other tests + framework.reload(); + env.setAuthorizationFramework(framework); + + Response response = target("file") + .path("defs") + .queryParam("path", validPath) + .request().get(); + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + // Cleanup. + env.setAuthorizationFramework(null); + } } diff --git a/opengrok-web/src/test/resources/testplugins.jar b/opengrok-web/src/test/resources/testplugins.jar new file mode 100644 index 00000000000..5037550abbd Binary files /dev/null and b/opengrok-web/src/test/resources/testplugins.jar differ