LCOV - code coverage report
Current view: top level - server/change - FileContentUtil.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 101 137 73.7 %
Date: 2022-11-19 15:00:39 Functions: 13 16 81.2 %

          Line data    Source code
       1             : // Copyright (C) 2013 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.change;
      16             : 
      17             : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
      18             : 
      19             : import com.google.common.base.Strings;
      20             : import com.google.common.hash.Hasher;
      21             : import com.google.common.hash.Hashing;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.common.data.PatchScript.FileMode;
      24             : import com.google.gerrit.entities.Patch;
      25             : import com.google.gerrit.extensions.restapi.BadRequestException;
      26             : import com.google.gerrit.extensions.restapi.BinaryResult;
      27             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      28             : import com.google.gerrit.server.git.GitRepositoryManager;
      29             : import com.google.gerrit.server.mime.FileTypeRegistry;
      30             : import com.google.gerrit.server.project.ProjectState;
      31             : import com.google.gerrit.server.util.time.TimeUtil;
      32             : import com.google.inject.Inject;
      33             : import com.google.inject.Singleton;
      34             : import eu.medsea.mimeutil.MimeType;
      35             : import java.io.IOException;
      36             : import java.io.OutputStream;
      37             : import java.security.SecureRandom;
      38             : import java.util.zip.ZipEntry;
      39             : import java.util.zip.ZipOutputStream;
      40             : import org.eclipse.jgit.errors.LargeObjectException;
      41             : import org.eclipse.jgit.errors.RepositoryNotFoundException;
      42             : import org.eclipse.jgit.lib.Constants;
      43             : import org.eclipse.jgit.lib.ObjectId;
      44             : import org.eclipse.jgit.lib.ObjectLoader;
      45             : import org.eclipse.jgit.lib.Repository;
      46             : import org.eclipse.jgit.revwalk.RevCommit;
      47             : import org.eclipse.jgit.revwalk.RevWalk;
      48             : import org.eclipse.jgit.treewalk.TreeWalk;
      49             : import org.eclipse.jgit.util.NB;
      50             : 
      51             : @Singleton
      52             : public class FileContentUtil {
      53             :   public static final String TEXT_X_GERRIT_COMMIT_MESSAGE = "text/x-gerrit-commit-message";
      54             :   public static final String TEXT_X_GERRIT_MERGE_LIST = "text/x-gerrit-merge-list";
      55             :   private static final String X_GIT_SYMLINK = "x-git/symlink";
      56             :   private static final String X_GIT_GITLINK = "x-git/gitlink";
      57             :   private static final int MAX_SIZE = 5 << 20;
      58             :   private static final String ZIP_TYPE = "application/zip";
      59         145 :   private static final SecureRandom rng = new SecureRandom();
      60             : 
      61             :   private final GitRepositoryManager repoManager;
      62             :   private final FileTypeRegistry registry;
      63             : 
      64             :   @Inject
      65         145 :   FileContentUtil(GitRepositoryManager repoManager, FileTypeRegistry ftr) {
      66         145 :     this.repoManager = repoManager;
      67         145 :     this.registry = ftr;
      68         145 :   }
      69             : 
      70             :   /**
      71             :    * Get the content of a file at a specific commit or one of it's parent commits.
      72             :    *
      73             :    * @param project A {@code Project} that this request refers to.
      74             :    * @param revstr An {@code ObjectId} specifying the commit.
      75             :    * @param path A string specifying the filepath.
      76             :    * @param parent A 1-based parent index to get the content from instead. Null if the content
      77             :    *     should be obtained from {@code revstr} instead.
      78             :    * @return Content of the file as {@code BinaryResult}.
      79             :    */
      80             :   public BinaryResult getContent(
      81             :       ProjectState project, ObjectId revstr, String path, @Nullable Integer parent)
      82             :       throws BadRequestException, ResourceNotFoundException, IOException {
      83          17 :     try (Repository repo = openRepository(project);
      84          17 :         RevWalk rw = new RevWalk(repo)) {
      85          17 :       if (parent != null) {
      86           1 :         RevCommit revCommit = rw.parseCommit(revstr);
      87           1 :         if (revCommit == null) {
      88           0 :           throw new ResourceNotFoundException("commit not found");
      89             :         }
      90           1 :         if (parent > revCommit.getParentCount()) {
      91           1 :           throw new BadRequestException("invalid parent");
      92             :         }
      93           1 :         revstr = rw.parseCommit(revstr).getParent(Integer.max(0, parent - 1)).toObjectId();
      94             :       }
      95          17 :       return getContent(repo, project, revstr, path);
      96             :     }
      97             :   }
      98             : 
      99             :   public BinaryResult getContent(
     100             :       Repository repo, ProjectState project, ObjectId revstr, String path)
     101             :       throws IOException, ResourceNotFoundException, BadRequestException {
     102          17 :     try (RevWalk rw = new RevWalk(repo)) {
     103          17 :       RevCommit commit = rw.parseCommit(revstr);
     104          17 :       try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, commit.getTree())) {
     105          17 :         if (tw == null) {
     106           4 :           throw new ResourceNotFoundException();
     107             :         }
     108             : 
     109          17 :         org.eclipse.jgit.lib.FileMode mode = tw.getFileMode(0);
     110          17 :         ObjectId id = tw.getObjectId(0);
     111          17 :         if (mode == org.eclipse.jgit.lib.FileMode.GITLINK) {
     112           0 :           return BinaryResult.create(id.name()).setContentType(X_GIT_GITLINK).base64();
     113             :         }
     114             : 
     115          17 :         if (mode == org.eclipse.jgit.lib.FileMode.TREE) {
     116           1 :           throw new BadRequestException("cannot retrieve content of directories");
     117             :         }
     118             : 
     119          17 :         ObjectLoader obj = repo.open(id, OBJ_BLOB);
     120             :         byte[] raw;
     121             :         try {
     122          17 :           raw = obj.getCachedBytes(MAX_SIZE);
     123           0 :         } catch (LargeObjectException e) {
     124           0 :           raw = null;
     125          17 :         }
     126             : 
     127             :         String type;
     128          17 :         if (mode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
     129           0 :           type = X_GIT_SYMLINK;
     130             :         } else {
     131          17 :           type = registry.getMimeType(path, raw).toString();
     132          17 :           type = resolveContentType(project, path, FileMode.FILE, type);
     133             :         }
     134             : 
     135          17 :         return asBinaryResult(raw, obj).setContentType(type).base64();
     136           0 :       }
     137           0 :     }
     138             :   }
     139             : 
     140             :   private static BinaryResult asBinaryResult(byte[] raw, ObjectLoader obj) {
     141          17 :     if (raw != null) {
     142          17 :       return BinaryResult.create(raw);
     143             :     }
     144           0 :     BinaryResult result =
     145           0 :         new BinaryResult() {
     146             :           @Override
     147             :           public void writeTo(OutputStream os) throws IOException {
     148           0 :             obj.copyTo(os);
     149           0 :           }
     150             :         };
     151           0 :     result.setContentLength(obj.getSize());
     152           0 :     return result;
     153             :   }
     154             : 
     155             :   public BinaryResult downloadContent(
     156             :       ProjectState project, ObjectId revstr, String path, @Nullable Integer parent)
     157             :       throws ResourceNotFoundException, IOException {
     158           1 :     try (Repository repo = openRepository(project);
     159           1 :         RevWalk rw = new RevWalk(repo)) {
     160           1 :       String suffix = "new";
     161           1 :       RevCommit commit = rw.parseCommit(revstr);
     162           1 :       if (parent != null && parent > 0) {
     163           0 :         if (commit.getParentCount() == 1) {
     164           0 :           suffix = "old";
     165             :         } else {
     166           0 :           suffix = "old" + parent;
     167             :         }
     168           0 :         commit = rw.parseCommit(commit.getParent(parent - 1));
     169             :       }
     170           1 :       try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, commit.getTree())) {
     171           1 :         if (tw == null) {
     172           0 :           throw new ResourceNotFoundException();
     173             :         }
     174             : 
     175           1 :         int mode = tw.getFileMode(0).getObjectType();
     176           1 :         if (mode != Constants.OBJ_BLOB) {
     177           0 :           throw new ResourceNotFoundException();
     178             :         }
     179             : 
     180           1 :         ObjectId id = tw.getObjectId(0);
     181           1 :         ObjectLoader obj = repo.open(id, OBJ_BLOB);
     182             :         byte[] raw;
     183             :         try {
     184           1 :           raw = obj.getCachedBytes(MAX_SIZE);
     185           0 :         } catch (LargeObjectException e) {
     186           0 :           raw = null;
     187           1 :         }
     188             : 
     189           1 :         MimeType contentType = registry.getMimeType(path, raw);
     190           1 :         return registry.isSafeInline(contentType)
     191           0 :             ? wrapBlob(path, obj, raw, contentType, suffix)
     192           1 :             : zipBlob(path, obj, commit, suffix);
     193             :       }
     194             :     }
     195             :   }
     196             : 
     197             :   private BinaryResult wrapBlob(
     198             :       String path,
     199             :       final ObjectLoader obj,
     200             :       byte[] raw,
     201             :       MimeType contentType,
     202             :       @Nullable String suffix) {
     203           0 :     return asBinaryResult(raw, obj)
     204           0 :         .setContentType(contentType.toString())
     205           0 :         .setAttachmentName(safeFileName(path, suffix));
     206             :   }
     207             : 
     208             :   @SuppressWarnings("resource")
     209             :   private BinaryResult zipBlob(
     210             :       final String path, ObjectLoader obj, RevCommit commit, @Nullable final String suffix) {
     211           1 :     final String commitName = commit.getName();
     212           1 :     final long when = commit.getCommitTime() * 1000L;
     213           1 :     return new BinaryResult() {
     214             :       @Override
     215             :       public void writeTo(OutputStream os) throws IOException {
     216           1 :         try (ZipOutputStream zipOut = new ZipOutputStream(os)) {
     217           1 :           String decoration = randSuffix();
     218           1 :           if (!Strings.isNullOrEmpty(suffix)) {
     219           1 :             decoration = suffix + '-' + decoration;
     220             :           }
     221           1 :           ZipEntry e = new ZipEntry(safeFileName(path, decoration));
     222           1 :           e.setComment(commitName + ":" + path);
     223           1 :           e.setSize(obj.getSize());
     224           1 :           e.setTime(when);
     225           1 :           zipOut.putNextEntry(e);
     226           1 :           obj.copyTo(zipOut);
     227           1 :           zipOut.closeEntry();
     228             :         }
     229           1 :       }
     230           1 :     }.setContentType(ZIP_TYPE).setAttachmentName(safeFileName(path, suffix) + ".zip").disableGzip();
     231             :   }
     232             : 
     233             :   private static String safeFileName(String fileName, @Nullable String suffix) {
     234             :     // Convert a file path (e.g. "src/Init.c") to a safe file name with
     235             :     // no meta-characters that might be unsafe on any given platform.
     236             :     //
     237           1 :     int slash = fileName.lastIndexOf('/');
     238           1 :     if (slash >= 0) {
     239           0 :       fileName = fileName.substring(slash + 1);
     240             :     }
     241             : 
     242           1 :     StringBuilder r = new StringBuilder(fileName.length());
     243           1 :     for (int i = 0; i < fileName.length(); i++) {
     244           1 :       final char c = fileName.charAt(i);
     245           1 :       if (c == '_' || c == '-' || c == '.' || c == '@') {
     246           1 :         r.append(c);
     247           1 :       } else if ('0' <= c && c <= '9') {
     248           0 :         r.append(c);
     249           1 :       } else if ('A' <= c && c <= 'Z') {
     250           0 :         r.append(c);
     251           1 :       } else if ('a' <= c && c <= 'z') {
     252           1 :         r.append(c);
     253           0 :       } else if (c == ' ' || c == '\n' || c == '\r' || c == '\t') {
     254           0 :         r.append('-');
     255             :       } else {
     256           0 :         r.append('_');
     257             :       }
     258             :     }
     259           1 :     fileName = r.toString();
     260             : 
     261           1 :     int ext = fileName.lastIndexOf('.');
     262           1 :     if (suffix == null) {
     263           0 :       return fileName;
     264           1 :     } else if (ext <= 0) {
     265           0 :       return fileName + "_" + suffix;
     266             :     } else {
     267           1 :       return fileName.substring(0, ext) + "_" + suffix + fileName.substring(ext);
     268             :     }
     269             :   }
     270             : 
     271             :   private static String randSuffix() {
     272             :     // Produce a random suffix that is difficult (or nearly impossible)
     273             :     // for an attacker to guess in advance. This reduces the risk that
     274             :     // an attacker could upload a *.class file and have us send a ZIP
     275             :     // that can be invoked through an applet tag in the victim's browser.
     276             :     //
     277           1 :     Hasher h = Hashing.murmur3_128().newHasher();
     278           1 :     byte[] buf = new byte[8];
     279             : 
     280           1 :     NB.encodeInt64(buf, 0, TimeUtil.nowMs());
     281           1 :     h.putBytes(buf);
     282             : 
     283           1 :     rng.nextBytes(buf);
     284           1 :     h.putBytes(buf);
     285             : 
     286           1 :     return h.hash().toString();
     287             :   }
     288             : 
     289             :   public static String resolveContentType(
     290             :       ProjectState project, String path, FileMode fileMode, String mimeType) {
     291          21 :     switch (fileMode) {
     292             :       case FILE:
     293          21 :         if (Patch.COMMIT_MSG.equals(path)) {
     294           4 :           return TEXT_X_GERRIT_COMMIT_MESSAGE;
     295             :         }
     296          21 :         if (Patch.MERGE_LIST.equals(path)) {
     297           3 :           return TEXT_X_GERRIT_MERGE_LIST;
     298             :         }
     299          20 :         if (project != null) {
     300          20 :           for (ProjectState p : project.tree()) {
     301          20 :             String t = p.getConfig().getMimeTypes().getMimeType(path);
     302          20 :             if (t != null) {
     303           0 :               return t;
     304             :             }
     305          20 :           }
     306             :         }
     307          20 :         return mimeType;
     308             :       case GITLINK:
     309           1 :         return X_GIT_GITLINK;
     310             :       case SYMLINK:
     311           0 :         return X_GIT_SYMLINK;
     312             :       default:
     313           0 :         throw new IllegalStateException("file mode: " + fileMode);
     314             :     }
     315             :   }
     316             : 
     317             :   private Repository openRepository(ProjectState project)
     318             :       throws RepositoryNotFoundException, IOException {
     319          17 :     return repoManager.openRepository(project.getNameKey());
     320             :   }
     321             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750