MiniDigger/Hangar

View on GitHub
orePlayCommon/app/ore/models/project/io/PluginFile.scala

Summary

Maintainability
A
1 hr
Test Coverage
package ore.models.project.io

import scala.language.higherKinds

import java.io._
import java.nio.file.{Files, Path}
import java.util.jar.{JarFile, JarInputStream}
import java.util.zip.{ZipEntry, ZipFile}

import scala.collection.JavaConverters._

import play.api.i18n.Messages

import ore.db.Model
import ore.models.user.{User, UserOwned}

import cats.data.EitherT
import cats.syntax.all._
import cats.effect.{Resource, Sync}

/**
  * Represents an uploaded plugin file.
  *
  * @param path Path to uploaded file
  */
class PluginFile(val path: Path, val user: Model[User]) {

  /**
    * Reads the temporary file's plugin meta file and returns the result.
    *
    * TODO: More validation on PluginMetadata results (null checks, etc)
    *
    * @return Plugin metadata or an error message
    */
  def loadMeta[F[_]](implicit messages: Messages, F: Sync[F]): EitherT[F, String, PluginFileWithData] = {
    val fileNames = PluginFileData.fileNames

    val res = newJarStream
      .flatMap { in =>
        val jarIn = F.delay(in.map(new JarInputStream(_)))
        Resource.make(jarIn) {
          case Right(is) => F.delay(is.close())
          case _         => F.unit
        }
      }
      .use { eJarIn =>
        F.delay {
          eJarIn.map { jarIn =>
            val fileDataSeq = Iterator
              .continually(jarIn.getNextJarEntry)
              .takeWhile(_ != null) // scalafix:ok
              .filter(entry => fileNames.contains(entry.getName))
              .flatMap(entry => PluginFileData.getData(entry.getName, new BufferedReader(new InputStreamReader(jarIn))))
              .toVector

            // Mainfest file isn't read in the jar stream for whatever reason
            // so we need to use the java API
            val manifestDataSeq = if (fileNames.contains(JarFile.MANIFEST_NAME)) {
              Option(jarIn.getManifest)
                .map { manifest =>
                  val manifestLines = new BufferedReader(
                    new StringReader(
                      manifest.getMainAttributes.asScala
                        .map(p => p._1.toString + ": " + p._2.toString)
                        .mkString("\n")
                    )
                  )

                  PluginFileData.getData(JarFile.MANIFEST_NAME, manifestLines)
                }
                .getOrElse(Nil)
            } else Nil

            val data = fileDataSeq ++ manifestDataSeq

            // This won't be called if a plugin uses mixins but doesn't
            // have a mcmod.info, but the check below will catch that
            if (data.isEmpty)
              Left(messages("error.plugin.metaNotFound"))
            else {
              val fileData = new PluginFileData(data)

              if (!fileData.isValidPlugin) Left(messages("error.plugin.incomplete", "id or version"))
              else Right(new PluginFileWithData(path, user, fileData))
            }
          }
        }
      }

    EitherT(res.map(_.flatMap(identity)))
  }

  /**
    * Returns a new [[InputStream]] for this [[PluginFile]]'s main JAR file.
    *
    * @return InputStream of JAR
    */
  def newJarStream[F[_]](implicit F: Sync[F]): Resource[F, Either[String, InputStream]] = {
    if (this.path.toString.endsWith(".jar"))
      Resource
        .fromAutoCloseable[F, InputStream](F.delay(Files.newInputStream(this.path)))
        .flatMap(is => Resource.pure(Right(is)))
    else
      Resource
        .fromAutoCloseable(F.delay(new ZipFile(this.path.toFile)))
        .flatMap { zip =>
          val jarIn = F.delay(findTopLevelJar(zip).map(zip.getInputStream))

          Resource.make(jarIn) {
            case Right(is) => F.delay(is.close())
            case _         => F.unit
          }
        }
  }

  private def findTopLevelJar(zip: ZipFile): Either[String, ZipEntry] = {
    val pluginEntry = zip.entries().asScala.find { entry =>
      val name = entry.getName
      !entry.isDirectory && name.split("/").length == 1 && name.endsWith(".jar")
    }

    pluginEntry.toRight("error.plugin.jarNotFound")
  }
}
object PluginFile {
  implicit val isUserOwned: UserOwned[PluginFile] = (a: PluginFile) => a.user.id.value
}