Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
src/main/java/org/olat/core/util/vfs/LocalFolderImpl.java
### 代码内容：
package org.olat.core.util.vfs;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;

import org.apache.logging.log4j.Logger;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.modules.bc.FolderConfig;
import org.olat.core.commons.services.vfs.VFSRepositoryModule;
import org.olat.core.commons.services.vfs.VFSRepositoryService;
import org.olat.core.id.Identity;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.Tracing;
import org.olat.core.logging.activity.ThreadLocalUserActivityLogger;
import org.olat.core.util.FileUtils;
import org.olat.core.util.vfs.filters.VFSItemFilter;
import org.olat.core.util.vfs.filters.VFSSystemItemFilter;
import org.olat.core.util.vfs.filters.VFSVersionsItemFilter;

/**
 * Description:

 * VFSContainer implementation that is based on a java.io.File from a local
 * filesystem. It implements a regular directory
 *
 *

 * Initial Date: 23.06.2005

 *
 * @author Felix Jost
 */
public class LocalFolderImpl extends LocalImpl implements VFSContainer {
	private static final Logger log = Tracing.createLoggerFor(LocalFolderImpl.class);

	private VFSItemFilter defaultFilter;

	/**
	 * @param folderfile
	 */
	private LocalFolderImpl() {
		super(null, null);
		throw new AssertException("Cannot instantiate LocalFolderImpl().");
	}

	/**
	 * Constructor
	 * @param folderFile The real file of type directory wrapped by this VFSContainer
	 */
	public LocalFolderImpl(File folderFile) {
		this(folderFile, null);
	}

	/**
	 * @param folderfile
	 */
	public LocalFolderImpl(File folderfile, VFSContainer parent) {
		super(folderfile, parent);
		boolean alreadyExists = folderfile.exists();
		boolean succesfullCreated = alreadyExists || folderfile.mkdirs();
		//check against concurrent creation of the folder, mkdirs return false if the directory exists
		if (!alreadyExists && !succesfullCreated && folderfile.exists()) {
			succesfullCreated = true;
		}
		if (!alreadyExists && !succesfullCreated) {
			throw new AssertException("Cannot create directory of LocalFolderImpl with reason (exists= ): "+alreadyExists+" && created= "+succesfullCreated+") path: " + folderfile.getAbsolutePath());
		}
	}

	@Override
	public List<VFSItem> getItems() {
		return getItems(null);
	}

	@Override
	public List<VFSItem> getItems(VFSItemFilter filter) {
		File aFolder = getBasefile();
		if(! aFolder.isDirectory()){
			throw new AssertException("basefile is not a directory: "+aFolder.getAbsolutePath());
		}
		File[] children = aFolder.listFiles();
		if(children == null) {
			children = new File[0];
		}
		int len = children.length;
		List<VFSItem> res = new ArrayList<>(len);

		for (int i = 0; i < len; i++) {
			File af = children[i];
			VFSItem item;
			if (af.isDirectory()) {
				LocalFolderImpl folderItem = new LocalFolderImpl(af, this);
				folderItem.setDefaultItemFilter(defaultFilter);
				item = folderItem;
			} else {
				item = new LocalFileImpl(af, this);
			}
			if ((defaultFilter == null || defaultFilter.accept(item))
					&& (filter == null || filter.accept(item))) {
				res.add(item);
			}
		}
		return res;
	}

	@Override
	public void setDefaultItemFilter(VFSItemFilter defaultFilter){
		this.defaultFilter = defaultFilter;
	}

	@Override
	public VFSStatus copyFrom(VFSItem source, Identity savedBy) {
		return copyFrom(source, true, savedBy);
	}

	@Override
	public VFSStatus copyContentOf(VFSContainer container, Identity savedBy) {
		VFSStatus status = VFSConstants.YES;
		for(VFSItem item:container.getItems(new VFSSystemItemFilter())) {
			status = copyFrom(item, true, savedBy);
		}
		return status;
	}

	/**
	 * Internal copy from, preventing quota checks on subfolders.
	 *
	 * @param source
	 * @param checkQuota
	 * @param savedBy
	 * @return
	 */
	private VFSStatus copyFrom(VFSItem source, boolean checkQuota, Identity savedBy) {
		if (source.canCopy() != VFSConstants.YES) {
			log.warn("Cannot copy file {} security denied", source);
			return VFSConstants.NO_SECURITY_DENIED;
		}

		String sourcename = source.getName();
		File basefile = getBasefile();

		// check if there is already an item with the same name...
		if (resolve(sourcename) != null) {
			log.warn("Cannot copy file {} name already used", sourcename);
			return VFSConstants.ERROR_NAME_ALREDY_USED;
		}

		// add either file bla.txt or folder blu as a child of this folder
		if (source instanceof VFSContainer) {
			// copy recursively
			VFSContainer sourcecontainer = (VFSContainer)source;
			// check if this is a containing container...
			if (VFSManager.isSelfOrParent(sourcecontainer, this)) {
				log.warn("Cannot copy file {}  overlapping", this);
				return VFSConstants.ERROR_OVERLAPPING;
			}

			// "copy" the container means creating a folder with that name
			// and let the children copy

			// create the folder
			LocalFolderImpl rootcopyfolder = new LocalFolderImpl(new File(basefile, sourcename), this);
			List<VFSItem> children = sourcecontainer.getItems(new VFSVersionsItemFilter());
			for (VFSItem chd:children) {
				VFSStatus status = rootcopyfolder.copyFrom(chd, false, savedBy);
				if (status != VFSConstants.SUCCESS) {
					log.warn("Cannot copy file {} with status {}", chd, status);
				}
			}
		} else if (source instanceof VFSLeaf) {
			// copy single item
			VFSLeaf s = (VFSLeaf) source;
			// check quota
			if (checkQuota) {
				long quotaLeft = VFSManager.getQuotaLeftKB(this);
				if(quotaLeft != Quota.UNLIMITED && quotaLeft < (s.getSize() / 1024)) {
					log.warn("Cannot copy file {} quota exceeded {}", s, quotaLeft);
					return VFSConstants.ERROR_QUOTA_EXCEEDED;
				}
			}

			File fTarget = new File(basefile, sourcename);
			try(InputStream in=s.getInputStream()) {
				FileUtils.bcopy(in, fTarget, "VFScopyFrom");
			} catch (Exception e) {
				return VFSConstants.ERROR_FAILED;
			}

			if(s.canMeta() == VFSConstants.YES || s.canVersion() == VFSConstants.YES) {
				VFSItem target = resolve(sourcename);
				if(target instanceof VFSLeaf && (target.canMeta() == VFSConstants.YES || s.canVersion() == VFSConstants.YES)) {
					VFSRepositoryService vfsRepositoryService = CoreSpringFactory.getImpl(VFSRepositoryService.class);
					vfsRepositoryService.itemSaved( (VFSLeaf)target, savedBy);
					vfsRepositoryService.copyTo(s, (VFSLeaf)target, this, savedBy);
				}
			}
		} else {
			throw new RuntimeException("neither a leaf nor a container!");
		}
		return VFSConstants.SUCCESS;
	}

	@Override
	public VFSStatus canWrite() {
		VFSContainer inheritingContainer = VFSManager.findInheritingSecurityCallbackContainer(this);
		if (inheritingContainer != null && !inheritingContainer.getLocalSecurityCallback().canWrite())
			return VFSConstants.NO_SECURITY_DENIED;
		return VFSConstants.YES;
	}

	@Override
	public VFSStatus canVersion() {
		return VFSRepositoryModule.canVersion(getBasefile());
	}

	@Override
	public VFSStatus rename(String newname) {
		CoreSpringFactory.getImpl(VFSRepositoryService.class).rename(this, newname);

		File f = getBasefile();
		File par = f.getParentFile();
		File nf = new File(par, newname);
		boolean ren = f.renameTo(nf);
		if (ren) {
			// f.renameTo() does NOT modify the path contained in the object f!!
			// The guys at sun consider this a feature and not a bug...
			// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4094022
			// We need to manually reload the new basefile and set it in our parent
			super.setBasefile(new File(nf.getAbsolutePath()));
			return VFSConstants.YES;
		} else {
			return VFSConstants.NO;
		}
	}

	@Override
	public VFSStatus delete() {
		if(!getBasefile().exists()) {
			return VFSConstants.YES;  // already non-existent
		}
		// we must empty the folders and subfolders first
		List<VFSItem> children = getItems();
		for (VFSItem child:children) {
			child.delete();
		}

		// Versioning makes a copy of the metadata, delete metadata after it
		if(canMeta() == VFSConstants.YES) {
			Identity identity = ThreadLocalUserActivityLogger.getLoggedIdentity();
			CoreSpringFactory.getImpl(VFSRepositoryService.class).markAsDeleted(this, identity);
		}
		// now delete the directory itself
		return deleteBasefile();
	}

	@Override
	public VFSStatus deleteSilently() {
		if(!getBasefile().exists()) {
			return VFSConstants.YES;  // already non-existent
		}
		// we must empty the folders and subfolders first
		List<VFSItem> children = getItems();
		for (VFSItem child:children) {
			child.deleteSilently();
		}

		if(canMeta() == VFSConstants.YES) {
			CoreSpringFactory.getImpl(VFSRepositoryService.class).deleteMetadata(getMetaInfo());
		}
		// now delete the directory itself
		return deleteBasefile();
	}

	private VFSStatus deleteBasefile() {
		VFSStatus status = VFSConstants.NO;
		try {
			// walk tree make sure the directory is deleted once all files,
			// versions files and others are properly deleted
			Files.walkFileTree(getBasefile().toPath(), new SimpleFileVisitor<Path>() {
				@Override
				public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
					Files.delete(file);
					return FileVisitResult.CONTINUE;
				}

				@Override
				public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
					Files.delete(dir);
					return FileVisitResult.CONTINUE;
				}
			});
			status = VFSConstants.YES;
		} catch(IOException e) {
			log.error("Cannot delete base file: " + this, e);
		}
		return status;
	}

	@Override
	public VFSItem resolve(String path) {
		VFSItem resolved = VFSManager.resolveFile(this, path);
		// set default filter on resolved file if it is a container
		if (resolved instanceof VFSContainer) {
			VFSContainer resolvedContainer = (VFSContainer) resolved;
			resolvedContainer.setDefaultItemFilter(defaultFilter);
		}
		return resolved;
	}

	@Override
	public boolean isInPath(String path) {
		Path bFile = getBasefile().toPath();
		Path filePath = bFile.resolve(path);
		Path normalizedPath = filePath.normalize();
		return normalizedPath.startsWith(bFile);
	}

	@Override
	public String getRelPath() {
		Path bFile = getBasefile().toPath();
		Path bcRoot = FolderConfig.getCanonicalRootPath();

		String relPath;
		if(bFile.startsWith(bcRoot)) {
			relPath = bcRoot.relativize(bFile).toString();
			if(relPath.endsWith("/")) {
				relPath = relPath.substring(0, relPath.length() - 1);
			}
			if(!relPath.startsWith("/")) {
				relPath = "/".concat(relPath);
			}
		} else {
			relPath = null;
		}
		return relPath;
	}

	@Override
	public String toString() {
		return "LFolder [base="+getBasefile()+"] ";
	}

	@Override
	public VFSContainer createChildContainer(String name) {
		File fNewFile = new File(getBasefile(), name);
		if(!isInPath(name)) {
			log.warn("Could not create a new container::{} in container::{} - file out of parent directory", name, getBasefile().getAbsolutePath());
			return null;
		}
		if (!fNewFile.mkdir()) {
			return null;
		}
		LocalFolderImpl locFI =  new LocalFolderImpl(fNewFile, this);
		locFI.setDefaultItemFilter(defaultFilter);
		return locFI;
	}

	@Override
	public VFSLeaf createChildLeaf(String name) {
		File fNewFile = new File(getBasefile(), name);
		try {
			if(!isInPath(name)) {
				log.warn("Could not create a new leaf::{} in container::{} - file out of parent directory", name, getBasefile().getAbsolutePath());
				return null;
			}
			if(!fNewFile.getParentFile().exists()) {
				fNewFile.getParentFile().mkdirs();
			}
			if (!fNewFile.createNewFile()) {
				log.warn("Could not create a new leaf::{} in container::{} - file alreay exists", name, getBasefile().getAbsolutePath());
				return null;
			}
		} catch (Exception e) {
			log.error("Error while creating child leaf::{} in container::{}", name, getBasefile().getAbsolutePath(), e);
			return null;
		}
		return new LocalFileImpl(fNewFile, this);
	}

	@Override
	public VFSItemFilter getDefaultItemFilter() {
		return defaultFilter;
	}
}

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】此代码存在路径遍历漏洞，具体表现为`createChildContainer`和`createChildLeaf`函数中未对传入的文件名进行验证。攻击者可以通过传入以斜杠（`/`）开头的文件名，创建文件或目录在根目录或其他不安全的位置，从而绕过安全限制。
【关键代码模式】`createChildContainer`、`createChildLeaf`、`File`构造函数、`mkdir`、`createNewFile`。
【修复方法说明】通过在创建子容器和子叶子之前调用`cleanFilename`函数，移除文件名开头的斜杠，从而限制文件或目录的创建位置在当前根目录下。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
pkg/api/plugins.go
### 代码内容：
package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana/pkg/api/dtos"
	"github.com/grafana/grafana/pkg/api/response"
	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/infra/fs"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/backendplugin"
	"github.com/grafana/grafana/pkg/plugins/manager/installer"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/web"
)

func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
	typeFilter := c.Query("type")
	enabledFilter := c.Query("enabled")
	embeddedFilter := c.Query("embedded")
	coreFilter := c.Query("core")

	// For users with viewer role we only return core plugins
	if !c.HasRole(models.ROLE_ADMIN) {
		coreFilter = "1"
	}

	pluginSettingsMap, err := hs.pluginSettings(c.Req.Context(), c.OrgId)
	if err != nil {
		return response.Error(500, "Failed to get list of plugins", err)
	}

	result := make(dtos.PluginList, 0)
	for _, pluginDef := range hs.pluginStore.Plugins(c.Req.Context()) {
		// filter out app sub plugins
		if embeddedFilter == "0" && pluginDef.IncludedInAppID != "" {
			continue
		}

		// filter out core plugins
		if (coreFilter == "0" && pluginDef.IsCorePlugin()) || (coreFilter == "1" && !pluginDef.IsCorePlugin()) {
			continue
		}

		// filter on type
		if typeFilter != "" && typeFilter != string(pluginDef.Type) {
			continue
		}

		if pluginDef.State == plugins.AlphaRelease && !hs.Cfg.PluginsEnableAlpha {
			continue
		}

		listItem := dtos.PluginListItem{
			Id:            pluginDef.ID,
			Name:          pluginDef.Name,
			Type:          string(pluginDef.Type),
			Category:      pluginDef.Category,
			Info:          pluginDef.Info,
			Dependencies:  pluginDef.Dependencies,
			LatestVersion: pluginDef.GrafanaComVersion,
			HasUpdate:     pluginDef.GrafanaComHasUpdate,
			DefaultNavUrl: pluginDef.DefaultNavURL,
			State:         pluginDef.State,
			Signature:     pluginDef.Signature,
			SignatureType: pluginDef.SignatureType,
			SignatureOrg:  pluginDef.SignatureOrg,
		}

		if pluginSetting, exists := pluginSettingsMap[pluginDef.ID]; exists {
			listItem.Enabled = pluginSetting.Enabled
			listItem.Pinned = pluginSetting.Pinned
		}

		if listItem.DefaultNavUrl == "" || !listItem.Enabled {
			listItem.DefaultNavUrl = hs.Cfg.AppSubURL + "/plugins/" + listItem.Id + "/"
		}

		// filter out disabled plugins
		if enabledFilter == "1" && !listItem.Enabled {
			continue
		}

		// filter out built in plugins
		if pluginDef.BuiltIn {
			continue
		}

		result = append(result, listItem)
	}

	sort.Sort(result)
	return response.JSON(200, result)
}

func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Response {
	pluginID := web.Params(c.Req)[":pluginId"]

	plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
	if !exists {
		return response.Error(404, "Plugin not found, no installed plugin with that id", nil)
	}

	dto := &dtos.PluginSetting{
		Type:          string(plugin.Type),
		Id:            plugin.ID,
		Name:          plugin.Name,
		Info:          plugin.Info,
		Dependencies:  plugin.Dependencies,
		Includes:      plugin.Includes,
		BaseUrl:       plugin.BaseURL,
		Module:        plugin.Module,
		DefaultNavUrl: plugin.DefaultNavURL,
		LatestVersion: plugin.GrafanaComVersion,
		HasUpdate:     plugin.GrafanaComHasUpdate,
		State:         plugin.State,
		Signature:     plugin.Signature,
		SignatureType: plugin.SignatureType,
		SignatureOrg:  plugin.SignatureOrg,
	}

	if plugin.IsApp() {
		dto.Enabled = plugin.AutoEnabled
		dto.Pinned = plugin.AutoEnabled
	}

	query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
	if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil {
		if !errors.Is(err, models.ErrPluginSettingNotFound) {
			return response.Error(500, "Failed to get login settings", nil)
		}
	} else {
		dto.Enabled = query.Result.Enabled
		dto.Pinned = query.Result.Pinned
		dto.JsonData = query.Result.JsonData
	}

	return response.JSON(200, dto)
}

func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Response {
	cmd := models.UpdatePluginSettingCmd{}
	if err := web.Bind(c.Req, &cmd); err != nil {
		return response.Error(http.StatusBadRequest, "bad request data", err)
	}
	pluginID := web.Params(c.Req)[":pluginId"]

	if _, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID); !exists {
		return response.Error(404, "Plugin not installed", nil)
	}

	cmd.OrgId = c.OrgId
	cmd.PluginId = pluginID
	if err := bus.DispatchCtx(c.Req.Context(), &cmd); err != nil {
		return response.Error(500, "Failed to update plugin setting", err)
	}

	return response.Success("Plugin settings updated")
}

func (hs *HTTPServer) GetPluginDashboards(c *models.ReqContext) response.Response {
	pluginID := web.Params(c.Req)[":pluginId"]

	list, err := hs.pluginDashboardManager.GetPluginDashboards(c.Req.Context(), c.OrgId, pluginID)
	if err != nil {
		var notFound plugins.NotFoundError
		if errors.As(err, &notFound) {
			return response.Error(404, notFound.Error(), nil)
		}

		return response.Error(500, "Failed to get plugin dashboards", err)
	}

	return response.JSON(200, list)
}

func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response {
	pluginID := web.Params(c.Req)[":pluginId"]
	name := web.Params(c.Req)[":name"]

	content, err := hs.pluginMarkdown(c.Req.Context(), pluginID, name)
	if err != nil {
		var notFound plugins.NotFoundError
		if errors.As(err, &notFound) {
			return response.Error(404, notFound.Error(), nil)
		}

		return response.Error(500, "Could not get markdown file", err)
	}

	// fallback try readme
	if len(content) == 0 {
		content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme")
		if err != nil {
			return response.Error(501, "Could not get markdown file", err)
		}
	}

	resp := response.Respond(200, content)
	resp.SetHeader("Content-Type", "text/plain; charset=utf-8")
	return resp
}

func (hs *HTTPServer) ImportDashboard(c *models.ReqContext) response.Response {
	apiCmd := dtos.ImportDashboardCommand{}
	if err := web.Bind(c.Req, &apiCmd); err != nil {
		return response.Error(http.StatusBadRequest, "bad request data", err)
	}
	var err error
	if apiCmd.PluginId == "" && apiCmd.Dashboard == nil {
		return response.Error(422, "Dashboard must be set", nil)
	}

	limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
	if err != nil {
		return response.Error(500, "failed to get quota", err)
	}
	if limitReached {
		return response.Error(403, "Quota reached", nil)
	}

	trimDefaults := c.QueryBoolWithDefault("trimdefaults", true)
	if trimDefaults && !hs.LoadSchemaService.IsDisabled() {
		apiCmd.Dashboard, err = hs.LoadSchemaService.DashboardApplyDefaults(apiCmd.Dashboard)
		if err != nil {
			return response.Error(500, "Error while applying default value to the dashboard json", err)
		}
	}

	dashInfo, dash, err := hs.pluginDashboardManager.ImportDashboard(c.Req.Context(), apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId,
		apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser)
	if err != nil {
		return hs.dashboardSaveErrorToApiResponse(c.Req.Context(), err)
	}

	err = hs.LibraryPanelService.ImportLibraryPanelsForDashboard(c.Req.Context(), c.SignedInUser, dash, apiCmd.FolderId)
	if err != nil {
		return response.Error(500, "Error while importing library panels", err)
	}

	err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c.Req.Context(), c.SignedInUser, dash)
	if err != nil {
		return response.Error(500, "Error while connecting library panels", err)
	}

	return response.JSON(200, dashInfo)
}

// CollectPluginMetrics collect metrics from a plugin.
//
// /api/plugins/:pluginId/metrics
func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Response {
	pluginID := web.Params(c.Req)[":pluginId"]
	plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
	if !exists {
		return response.Error(404, "Plugin not found", nil)
	}

	resp, err := hs.pluginClient.CollectMetrics(c.Req.Context(), plugin.ID)
	if err != nil {
		return translatePluginRequestErrorToAPIError(err)
	}

	headers := make(http.Header)
	headers.Set("Content-Type", "text/plain")

	return response.CreateNormalResponse(headers, resp.PrometheusMetrics, http.StatusOK)
}

// getPluginAssets returns public plugin assets (images, JS, etc.)
//
// /public/plugins/:pluginId/*
func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) {
	pluginID := web.Params(c.Req)[":pluginId"]
	plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
	if !exists {
		c.JsonApiErr(404, "Plugin not found", nil)
		return
	}

	// prepend slash for cleaning relative paths
	requestedFile := filepath.Clean(filepath.Join("/", web.Params(c.Req)["*"]))
	rel, err := filepath.Rel("/", requestedFile)
	if err != nil {
		// slash is prepended above therefore this is not expected to fail
		c.JsonApiErr(500, "Failed to get the relative path", err)
		return
	}

	if !plugin.IncludedInSignature(rel) {
		hs.log.Warn("Access to requested plugin file will be forbidden in upcoming Grafana versions as the file "+
			"is not included in the plugin signature", "file", requestedFile)
	}

	absPluginDir, err := filepath.Abs(plugin.PluginDir)
	if err != nil {
		c.JsonApiErr(500, "Failed to get plugin absolute path", nil)
		return
	}

	pluginFilePath := filepath.Join(absPluginDir, rel)
	// It's safe to ignore gosec warning G304 since we already clean the requested file path and subsequently
	// use this with a prefix of the plugin's directory, which is set during plugin loading
	// nolint:gosec
	f, err := os.Open(pluginFilePath)
	if err != nil {
		if os.IsNotExist(err) {
			c.JsonApiErr(404, "Plugin file not found", err)
			return
		}
		c.JsonApiErr(500, "Could not open plugin file", err)
		return
	}
	defer func() {
		if err := f.Close(); err != nil {
			hs.log.Error("Failed to close file", "err", err)
		}
	}()

	fi, err := f.Stat()
	if err != nil {
		c.JsonApiErr(500, "Plugin file exists but could not open", err)
		return
	}

	if hs.Cfg.Env == setting.Dev {
		c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
	} else {
		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
	}

	http.ServeContent(c.Resp, c.Req, pluginFilePath, fi.ModTime(), f)
}

// CheckHealth returns the health of a plugin.
// /api/plugins/:pluginId/health
func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response {
	pluginID := web.Params(c.Req)[":pluginId"]

	pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, "", c.SignedInUser, false)
	if err != nil {
		return response.Error(500, "Failed to get plugin settings", err)
	}
	if !found {
		return response.Error(404, "Plugin not found", nil)
	}

	resp, err := hs.pluginClient.CheckHealth(c.Req.Context(), &backend.CheckHealthRequest{
		PluginContext: pCtx,
	})
	if err != nil {
		return translatePluginRequestErrorToAPIError(err)
	}

	payload := map[string]interface{}{
		"status":  resp.Status.String(),
		"message": resp.Message,
	}

	// Unmarshal JSONDetails if it's not empty.
	if len(resp.JSONDetails) > 0 {
		var jsonDetails map[string]interface{}
		err = json.Unmarshal(resp.JSONDetails, &jsonDetails)
		if err != nil {
			return response.Error(500, "Failed to unmarshal detailed response from backend plugin", err)
		}

		payload["details"] = jsonDetails
	}

	if resp.Status != backend.HealthStatusOk {
		return response.JSON(503, payload)
	}

	return response.JSON(200, payload)
}

// CallResource passes a resource call from a plugin to the backend plugin.
//
// /api/plugins/:pluginId/resources/*
func (hs *HTTPServer) CallResource(c *models.ReqContext) {
	pluginID := web.Params(c.Req)[":pluginId"]

	pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, "", c.SignedInUser, false)
	if err != nil {
		c.JsonApiErr(500, "Failed to get plugin settings", err)
		return
	}
	if !found {
		c.JsonApiErr(404, "Plugin not found", nil)
		return
	}
	hs.pluginClient.CallResource(pCtx, c, web.Params(c.Req)["*"])
}

func (hs *HTTPServer) GetPluginErrorsList(_ *models.ReqContext) response.Response {
	return response.JSON(200, hs.pluginErrorResolver.PluginErrors())
}

func (hs *HTTPServer) InstallPlugin(c *models.ReqContext) response.Response {
	dto := dtos.InstallPluginCommand{}
	if err := web.Bind(c.Req, &dto); err != nil {
		return response.Error(http.StatusBadRequest, "bad request data", err)
	}
	pluginID := web.Params(c.Req)[":pluginId"]

	err := hs.pluginStore.Add(c.Req.Context(), pluginID, dto.Version, plugins.AddOpts{})
	if err != nil {
		var dupeErr plugins.DuplicateError
		if errors.As(err, &dupeErr) {
			return response.Error(http.StatusConflict, "Plugin already installed", err)
		}
		var versionUnsupportedErr installer.ErrVersionUnsupported
		if errors.As(err, &versionUnsupportedErr) {
			return response.Error(http.StatusConflict, "Plugin version not supported", err)
		}
		var versionNotFoundErr installer.ErrVersionNotFound
		if errors.As(err, &versionNotFoundErr) {
			return response.Error(http.StatusNotFound, "Plugin version not found", err)
		}
		var clientError installer.Response4xxError
		if errors.As(err, &clientError) {
			return response.Error(clientError.StatusCode, clientError.Message, err)
		}
		if errors.Is(err, plugins.ErrInstallCorePlugin) {
			return response.Error(http.StatusForbidden, "Cannot install or change a Core plugin", err)
		}

		return response.Error(http.StatusInternalServerError, "Failed to install plugin", err)
	}

	return response.JSON(http.StatusOK, []byte{})
}

func (hs *HTTPServer) UninstallPlugin(c *models.ReqContext) response.Response {
	pluginID := web.Params(c.Req)[":pluginId"]

	err := hs.pluginStore.Remove(c.Req.Context(), pluginID)
	if err != nil {
		if errors.Is(err, plugins.ErrPluginNotInstalled) {
			return response.Error(http.StatusNotFound, "Plugin not installed", err)
		}
		if errors.Is(err, plugins.ErrUninstallCorePlugin) {
			return response.Error(http.StatusForbidden, "Cannot uninstall a Core plugin", err)
		}
		if errors.Is(err, plugins.ErrUninstallOutsideOfPluginDir) {
			return response.Error(http.StatusForbidden, "Cannot uninstall a plugin outside of the plugins directory", err)
		}

		return response.Error(http.StatusInternalServerError, "Failed to uninstall plugin", err)
	}
	return response.JSON(http.StatusOK, []byte{})
}

func translatePluginRequestErrorToAPIError(err error) response.Response {
	if errors.Is(err, backendplugin.ErrPluginNotRegistered) {
		return response.Error(404, "Plugin not found", err)
	}

	if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
		return response.Error(404, "Not found", err)
	}

	if errors.Is(err, backendplugin.ErrHealthCheckFailed) {
		return response.Error(500, "Plugin health check failed", err)
	}

	if errors.Is(err, backendplugin.ErrPluginUnavailable) {
		return response.Error(503, "Plugin unavailable", err)
	}

	return response.Error(500, "Plugin request failed", err)
}

func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginId string, name string) ([]byte, error) {
	plugin, exists := hs.pluginStore.Plugin(ctx, pluginId)
	if !exists {
		return nil, plugins.NotFoundError{PluginID: pluginId}
	}

	// nolint:gosec
	// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
	// on plugin the folder structure on disk and not user input.
	path := filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
	exists, err := fs.Exists(path)
	if err != nil {
		return nil, err
	}
	if !exists {
		path = filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
	}

	exists, err = fs.Exists(path)
	if err != nil {
		return nil, err
	}
	if !exists {
		return make([]byte, 0), nil
	}

	// nolint:gosec
	// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
	// on plugin the folder structure on disk and not user input.
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, err
	}
	return data, nil
}

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】原始代码中的`getPluginAssets`函数存在路径遍历漏洞。攻击者可以通过构造恶意的`*`参数来访问插件目录之外的文件。具体表现为未对用户输入的文件路径进行充分验证和清理。
【关键代码模式】`filepath.Clean(filepath.Join("/", web.Params(c.Req)["*"]))` - 这部分代码尝试清理用户输入的文件路径，但由于后续的文件操作仍然直接使用未验证的路径，因此仍然存在安全风险。
【修复方法说明】修复方法通过将用户输入的文件路径（`name`）与`mdFilepath`函数的处理相结合，确保文件路径的清理和验证。


Instruction:
作为 Java 安全专家，检测此代码中的安全漏洞：
## 内容1
### 文件路径：
core/src/main/java/com/linecorp/armeria/internal/common/ArmeriaHttpUtil.java
### 代码内容：
package com.linecorp.armeria.internal.common;

import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static io.netty.util.AsciiString.EMPTY_STRING;
import static io.netty.util.ByteProcessor.FIND_COMMA;
import static io.netty.util.internal.StringUtil.decodeHexNibble;
import static java.util.Objects.requireNonNull;

import java.net.InetSocketAddress;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.BiConsumer;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;

import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.Http1HeaderNaming;
import com.linecorp.armeria.common.HttpData;
import com.linecorp.armeria.common.HttpHeaderNames;
import com.linecorp.armeria.common.HttpHeaders;
import com.linecorp.armeria.common.HttpHeadersBuilder;
import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.RequestHeaders;
import com.linecorp.armeria.common.RequestHeadersBuilder;
import com.linecorp.armeria.common.ResponseHeaders;
import com.linecorp.armeria.common.ResponseHeadersBuilder;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.Version;
import com.linecorp.armeria.internal.common.util.TemporaryThreadLocals;
import com.linecorp.armeria.server.ServerConfig;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.DefaultHeaders;
import io.netty.handler.codec.UnsupportedValueConverter;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames;
import io.netty.util.AsciiString;
import io.netty.util.HashingStrategy;
import io.netty.util.internal.StringUtil;

/**
 * Provides various utility functions for internal use related with HTTP.
 *
 *
The conversion between HTTP/1 and HTTP/2 has been forked from Netty's {@link HttpConversionUtil}.
 */
public final class ArmeriaHttpUtil {

    // Forked from Netty 4.1.34 at 4921f62c8ab8205fd222439dcd1811760b05daf1

    /**
     * The default case-insensitive {@link AsciiString} hasher and comparator for HTTP/2 headers.
     */
    private static final HashingStrategy<AsciiString> HTTP2_HEADER_NAME_HASHER =
            new HashingStrategy<AsciiString>() {
                @Override
                public int hashCode(AsciiString o) {
                    return o.hashCode();
                }

                @Override
                public boolean equals(AsciiString a, AsciiString b) {
                    return a.contentEqualsIgnoreCase(b);
                }
            };

    /**
     * The default HTTP content-type charset.
     *
     *
Note that we use {@link StandardCharsets#UTF_8} as default because it is common practice even though
     * it's not the HTTP standard.
     */
    public static final Charset HTTP_DEFAULT_CONTENT_CHARSET = StandardCharsets.UTF_8;

    /**
     * The old {@code "proxy-connection"} header which has been superceded by {@code "connection"}.
     */
    public static final AsciiString HEADER_NAME_PROXY_CONNECTION = AsciiString.cached("proxy-connection");

    /**
     * The set of headers that should not be directly copied when converting headers from HTTP/1 to HTTP/2.
     */
    private static final CaseInsensitiveMap HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST = new CaseInsensitiveMap();

    /**
     * The set of headers that should not be directly copied when converting headers from HTTP/2 to HTTP/1.
     */
    private static final CaseInsensitiveMap HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST = new CaseInsensitiveMap();

    /**
     * The set of headers that must not be directly copied when converting trailers.
     */
    private static final CaseInsensitiveMap HTTP_TRAILER_DISALLOWED_LIST = new CaseInsensitiveMap();

    static {
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.CONNECTION, EMPTY_STRING);
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.KEEP_ALIVE, EMPTY_STRING);
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(HEADER_NAME_PROXY_CONNECTION, EMPTY_STRING);
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING);
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
        HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);

        // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.AUTHORITY, EMPTY_STRING);
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.METHOD, EMPTY_STRING);
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.PATH, EMPTY_STRING);
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.SCHEME, EMPTY_STRING);
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.STATUS, EMPTY_STRING);

        // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1
        // The "chunked" transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2.
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);

        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING);
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING);
        HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.add(ExtensionHeaderNames.PATH.text(), EMPTY_STRING);

        // https://datatracker.ietf.org/doc/html/rfc7230#section-4.1.2
        // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1
        // A sender MUST NOT generate a trailer that contains a field necessary for message framing:
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.CONTENT_LENGTH, EMPTY_STRING);

        // for request modifiers:
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.CACHE_CONTROL, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.EXPECT, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.HOST, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.MAX_FORWARDS, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.PRAGMA, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.RANGE, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.TE, EMPTY_STRING);

        // for authentication:
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.WWW_AUTHENTICATE, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.AUTHORIZATION, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.PROXY_AUTHENTICATE, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.PROXY_AUTHORIZATION, EMPTY_STRING);

        // for response control data:
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.DATE, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.LOCATION, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.RETRY_AFTER, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.VARY, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.WARNING, EMPTY_STRING);

        // or for determining how to process the payload:
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.CONTENT_ENCODING, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.CONTENT_TYPE, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.CONTENT_RANGE, EMPTY_STRING);
        HTTP_TRAILER_DISALLOWED_LIST.add(HttpHeaderNames.TRAILER, EMPTY_STRING);
    }

    static final Set<AsciiString> ADDITIONAL_REQUEST_HEADER_DISALLOWED_LIST = ImmutableSet.of(
            HttpHeaderNames.SCHEME, HttpHeaderNames.STATUS, HttpHeaderNames.METHOD, HttpHeaderNames.AUTHORITY);

    private static final Set<AsciiString> REQUEST_PSEUDO_HEADERS = ImmutableSet.of(
            HttpHeaderNames.METHOD, HttpHeaderNames.SCHEME, HttpHeaderNames.AUTHORITY,
            HttpHeaderNames.PATH, HttpHeaderNames.PROTOCOL);

    private static final Set<AsciiString> PSEUDO_HEADERS = ImmutableSet.<AsciiString>builder()
                                                                       .addAll(REQUEST_PSEUDO_HEADERS)
                                                                       .add(HttpHeaderNames.STATUS)
                                                                       .build();

    public static final String SERVER_HEADER =
            "Armeria/" + Version.get("armeria", ArmeriaHttpUtil.class.getClassLoader())
                                .artifactVersion();

    /**
     * Translations from HTTP/2 header name to the HTTP/1.x equivalent. Currently, we expect these headers to
     * only allow a single value in the request. If adding headers that can potentially have multiple values,
     * please check the usage in code accordingly.
     */
    private static final CaseInsensitiveMap REQUEST_HEADER_TRANSLATIONS = new CaseInsensitiveMap();

    static {
        REQUEST_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
                                        HttpHeaderNames.HOST);
    }

    /**
     * rfc7540, 8.1.2.3 states the path must not
     * be empty, and instead should be {@code /}.
     */
    private static final String EMPTY_REQUEST_PATH = "/";

    private static final Splitter COOKIE_SPLITTER = Splitter.on(';').trimResults().omitEmptyStrings();
    private static final String COOKIE_SEPARATOR = "; ";
    private static final Joiner COOKIE_JOINER = Joiner.on(COOKIE_SEPARATOR);

    @Nullable
    private static final LoadingCache<AsciiString, String> HEADER_VALUE_CACHE =
            Flags.headerValueCacheSpec() != null ? buildCache(Flags.headerValueCacheSpec()) : null;
    private static final Set<AsciiString> CACHED_HEADERS = Flags.cachedHeaders().stream().map(AsciiString::of)
                                                                .collect(toImmutableSet());

    private static LoadingCache<AsciiString, String> buildCache(String spec) {
        return Caffeine.from(spec).build(AsciiString::toString);
    }

    /**
     * Concatenates two path strings.
     */
    public static String concatPaths(@Nullable String path1, @Nullable String path2) {
        path2 = path2 == null ? "" : path2;

        if (path1 == null || path1.isEmpty() || EMPTY_REQUEST_PATH.equals(path1)) {
            if (path2.isEmpty()) {
                return EMPTY_REQUEST_PATH;
            }

            if (path2.charAt(0) == '/') {
                return path2; // Most requests will land here.
            }

            return '/' + path2;
        }

        // At this point, we are sure path1 is neither empty nor null.
        if (path2.isEmpty()) {
            // Only path1 is non-empty. No need to concatenate.
            return path1;
        }

        if (path1.charAt(path1.length() - 1) == '/') {
            if (path2.charAt(0) == '/') {
                // path1 ends with '/' and path2 starts with '/'.
                // Avoid double-slash by stripping the first slash of path2.
                return new StringBuilder(path1.length() + path2.length() - 1)
                        .append(path1).append(path2, 1, path2.length()).toString();
            }

            // path1 ends with '/' and path2 does not start with '/'.
            // Simple concatenation would suffice.
            return path1 + path2;
        }

        if (path2.charAt(0) == '/' || path2.charAt(0) == '?') {
            // path1 does not end with '/' and path2 starts with '/' or '?'
            // Simple concatenation would suffice.
            return path1 + path2;
        }

        // path1 does not end with '/' and path2 does not start with '/' or '?'.
        // Need to insert '/' between path1 and path2.
        return path1 + '/' + path2;
    }

    /**
     * Decodes a percent-encoded path string.
     */
    public static String decodePath(String path) {
        if (path.indexOf('%') < 0) {
            // No need to decoded; not percent-encoded
            return path;
        }

        // Decode percent-encoded characters.
        // An invalid character is replaced with 0xFF, which will be replaced into '�' by UTF-8 decoder.
        final int len = path.length();
        try (TemporaryThreadLocals tempThreadLocals = TemporaryThreadLocals.acquire()) {
            final byte[] buf = tempThreadLocals.byteArray(len);
            int dstLen = 0;
            for (int i = 0; i < len; i++) {
                final char ch = path.charAt(i);
                if (ch != '%') {
                    buf[dstLen++] = (byte) ((ch & 0xFF80) == 0 ? ch : 0xFF);
                    continue;
                }

                // Decode a percent-encoded character.
                final int hexEnd = i + 3;
                if (hexEnd > len) {
                    // '%' or '%x' (must be followed by two hexadigits)
                    buf[dstLen++] = (byte) 0xFF;
                    break;
                }

                final int digit1 = decodeHexNibble(path.charAt(++i));
                final int digit2 = decodeHexNibble(path.charAt(++i));
                if (digit1 < 0 || digit2 < 0) {
                    // The first or second digit is not hexadecimal.
                    buf[dstLen++] = (byte) 0xFF;
                } else {
                    buf[dstLen++] = (byte) ((digit1 << 4) | digit2);
                }
            }

            return new String(buf, 0, dstLen, StandardCharsets.UTF_8);
        }
    }

    /**
     * Returns {@code true} if the specified {@code path} is an absolute {@code URI}.
     */
    public static boolean isAbsoluteUri(@Nullable String maybeUri) {
        if (maybeUri == null) {
            return false;
        }
        final int firstColonIdx = maybeUri.indexOf(':');
        if (firstColonIdx <= 0 || firstColonIdx + 3 >= maybeUri.length()) {
            return false;
        }
        final int firstSlashIdx = maybeUri.indexOf('/');
        if (firstSlashIdx <= 0 || firstSlashIdx < firstColonIdx) {
            return false;
        }

        return maybeUri.charAt(firstColonIdx + 1) == '/' && maybeUri.charAt(firstColonIdx + 2) == '/';
    }

    /**
     * Returns {@code true} if the specified HTTP status string represents an informational status.
     */
    public static boolean isInformational(@Nullable String statusText) {
        return statusText != null && !statusText.isEmpty() && statusText.charAt(0) == '1';
    }

    /**
     * Returns {@code true} if the content of the response with the given {@link HttpStatus} is one of
     * {@link HttpStatus#NO_CONTENT}, {@link HttpStatus#RESET_CONTENT} and {@link HttpStatus#NOT_MODIFIED}.
     *
     * @throws IllegalArgumentException if the specified {@code content} is not empty when the specified
     *                                  {@link HttpStatus} is one of {@link HttpStatus#NO_CONTENT},
     *                                  {@link HttpStatus#RESET_CONTENT} and {@link HttpStatus#NOT_MODIFIED}.
     */
    public static boolean isContentAlwaysEmptyWithValidation(HttpStatus status, HttpData content) {
        if (!status.isContentAlwaysEmpty()) {
            return false;
        }

        if (!content.isEmpty()) {
            throw new IllegalArgumentException(
                    "A " + status + " response must have empty content: " + content.length() + " byte(s)");
        }

        return true;
    }

    /**
     * Returns {@code true} if the specified {@code request} is a CORS preflight request.
     */
    public static boolean isCorsPreflightRequest(com.linecorp.armeria.common.HttpRequest request) {
        requireNonNull(request, "request");
        return request.method() == HttpMethod.OPTIONS &&
               request.headers().contains(HttpHeaderNames.ORIGIN) &&
               request.headers().contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD);
    }

    /**
     * Returns the disallowed response headers.
     */
    @VisibleForTesting
    static Set<AsciiString> disallowedResponseHeaderNames() {
        // Request Pseudo-Headers are not allowed for response headers.
        // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3
        return REQUEST_PSEUDO_HEADERS;
    }

    /**
     * Parses the specified HTTP header directives and invokes the specified {@code callback}
     * with the directive names and values.
     */
    public static void parseDirectives(String directives, BiConsumer<String, String> callback) {
        final int len = directives.length();
        for (int i = 0; i < len;) {
            final int nameStart = i;
            final String name;
            final String value;

            // Find the name.
            for (; i < len; i++) {
                final char ch = directives.charAt(i);
                if (ch == ',' || ch == '=') {
                    break;
                }
            }
            name = directives.substring(nameStart, i).trim();

            // Find the value.
            if (i == len || directives.charAt(i) == ',') {
                // Skip comma or go beyond 'len' to break the loop.
                i++;
                value = null;
            } else {
                // Skip '='.
                i++;

                // Skip whitespaces.
                for (; i < len; i++) {
                    final char ch = directives.charAt(i);
                    if (ch != ' ' && ch != '\t') {
                        break;
                    }
                }

                if (i < len && directives.charAt(i) == '\"') {
                    // Handle quoted string.
                    // Skip the opening quote.
                    i++;
                    final int valueStart = i;

                    // Find the closing quote.
                    for (; i < len; i++) {
                        if (directives.charAt(i) == '\"') {
                            break;
                        }
                    }
                    value = directives.substring(valueStart, i);

                    // Skip the closing quote.
                    i++;

                    // Find the comma and skip it.
                    for (; i < len; i++) {
                        if (directives.charAt(i) == ',') {
                            i++;
                            break;
                        }
                    }
                } else {
                    // Handle unquoted string.
                    final int valueStart = i;

                    // Find the comma.
                    for (; i < len; i++) {
                        if (directives.charAt(i) == ',') {
                            break;
                        }
                    }
                    value = directives.substring(valueStart, i).trim();

                    // Skip the comma.
                    i++;
                }
            }

            if (!name.isEmpty()) {
                callback.accept(Ascii.toLowerCase(name), Strings.emptyToNull(value));
            }
        }
    }

    /**
     * Converts the specified HTTP header directive value into a long integer.
     *
     * @return the converted value if {@code value} is equal to or greater than {@code 0}.
     *         {@code -1} otherwise, i.e. if a negative integer or not a number.
     */
    public static long parseDirectiveValueAsSeconds(@Nullable String value) {
        if (value == null) {
            return -1;
        }

        try {
            final long converted = Long.parseLong(value);
            return converted >= 0 ? converted : -1;
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    /**
     * Converts the specified Netty HTTP/2 into Armeria HTTP/2 {@link RequestHeaders}.
     */
    public static RequestHeaders toArmeriaRequestHeaders(ChannelHandlerContext ctx, Http2Headers headers,
                                                         boolean endOfStream, String scheme,
                                                         ServerConfig cfg) {
        assert headers instanceof ArmeriaHttp2Headers;
        final HttpHeadersBuilder builder = ((ArmeriaHttp2Headers) headers).delegate();
        builder.endOfStream(endOfStream);
        // A CONNECT request might not have ":scheme". See https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.3
        if (!builder.contains(HttpHeaderNames.SCHEME)) {
            builder.add(HttpHeaderNames.SCHEME, scheme);
        }

        if (builder.get(HttpHeaderNames.AUTHORITY) == null && builder.get(HttpHeaderNames.HOST) == null) {
            final String defaultHostname = cfg.defaultVirtualHost().defaultHostname();
            final int port = ((InetSocketAddress) ctx.channel().localAddress()).getPort();
            builder.add(HttpHeaderNames.AUTHORITY, defaultHostname + ':' + port);
        }
        final List<String> cookies = builder.getAll(HttpHeaderNames.COOKIE);
        if (cookies.size() > 1) {
            // Cookies must be concatenated into a single octet string.
            // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
            builder.set(HttpHeaderNames.COOKIE, COOKIE_JOINER.join(cookies));
        }
        return RequestHeaders.of(builder.build());
    }

    /**
     * Converts the specified Netty HTTP/2 into Armeria HTTP/2 headers.
     */
    public static HttpHeaders toArmeria(Http2Headers http2Headers, boolean request, boolean endOfStream) {
        assert http2Headers instanceof ArmeriaHttp2Headers;
        final HttpHeadersBuilder delegate = ((ArmeriaHttp2Headers) http2Headers).delegate();
        delegate.endOfStream(endOfStream);
        HttpHeaders headers = delegate.build();

        if (request) {
            if (headers.contains(HttpHeaderNames.METHOD)) {
                headers = RequestHeaders.of(headers);
            }
            // http2Headers should be a trailers
        } else {
            if (headers.contains(HttpHeaderNames.STATUS)) {
                headers = ResponseHeaders.of(headers);
            }
        }
        return headers;
    }

    /**
     * Converts the headers of the given Netty HTTP/1.x request into Armeria HTTP/2 headers.
     * The following headers are only used if they can not be found in the {@code HOST} header or the
     * {@code Request-Line} as defined by rfc7230
     *

     *
{@link ExtensionHeaderNames#SCHEME}

     *

     * {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}.
     */
    public static RequestHeaders toArmeria(ChannelHandlerContext ctx, HttpRequest in,
                                           ServerConfig cfg, String scheme) throws URISyntaxException {

        final String path = in.uri();
        if (path.charAt(0) != '/' && !"*".equals(path)) {
            // We support only origin form and asterisk form.
            throw new URISyntaxException(path, "neither origin form nor asterisk form");
        }

        final io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
        final RequestHeadersBuilder out = RequestHeaders.builder();
        out.sizeHint(inHeaders.size());
        out.method(HttpMethod.valueOf(in.method().name()))
           .path(path)
           .scheme(scheme);

        // Add the HTTP headers which have not been consumed above
        toArmeria(inHeaders, out);
        if (!out.contains(HttpHeaderNames.HOST)) {
            // The client violates the spec that the request headers must contain a Host header.
            // But we just add Host header to allow the request.
            // https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
            final String defaultHostname = cfg.defaultVirtualHost().defaultHostname();
            final int port = ((InetSocketAddress) ctx.channel().localAddress()).getPort();
            out.add(HttpHeaderNames.HOST, defaultHostname + ':' + port);
        }
        return out.build();
    }

    /**
     * Converts the headers of the given Netty HTTP/1.x response into Armeria HTTP/2 headers.
     */
    public static ResponseHeaders toArmeria(HttpResponse in) {
        final io.netty.handler.codec.http.HttpHeaders inHeaders = in.headers();
        final ResponseHeadersBuilder out = ResponseHeaders.builder();
        out.sizeHint(inHeaders.size());
        out.status(HttpStatus.valueOf(in.status().code()));
        // Add the HTTP headers which have not been consumed above
        toArmeria(inHeaders, out);
        return out.build();
    }

    /**
     * Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
     */
    public static HttpHeaders toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders) {
        if (inHeaders.isEmpty()) {
            return HttpHeaders.of();
        }

        final HttpHeadersBuilder out = HttpHeaders.builder();
        out.sizeHint(inHeaders.size());
        toArmeria(inHeaders, out);
        return out.build();
    }

    /**
     * Converts the specified Netty HTTP/1 headers into Armeria HTTP/2 headers.
     */
    public static void toArmeria(io.netty.handler.codec.http.HttpHeaders inHeaders, HttpHeadersBuilder out) {
        final Iterator<Entry<CharSequence, CharSequence>> iter = inHeaders.iteratorCharSequence();
        // Choose 8 as a default size because it is unlikely we will see more than 4 Connection headers values,
        // but still allowing for "enough" space in the map to reduce the chance of hash code collision.
        final CaseInsensitiveMap connectionDisallowedList =
                toLowercaseMap(inHeaders.valueCharSequenceIterator(HttpHeaderNames.CONNECTION), 8);
        StringJoiner cookieJoiner = null;
        while (iter.hasNext()) {
            final Entry<CharSequence, CharSequence> entry = iter.next();
            final AsciiString aName = HttpHeaderNames.of(entry.getKey()).toLowerCase();
            if (HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.contains(aName) ||
                connectionDisallowedList.contains(aName)) {
                continue;
            }

            // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 makes a special exception for TE
            if (aName.equals(HttpHeaderNames.TE)) {
                toHttp2HeadersFilterTE(entry, out);
                continue;
            }

            // Cookies must be concatenated into a single octet string.
            // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
            final CharSequence value = entry.getValue();
            if (aName.equals(HttpHeaderNames.COOKIE)) {
                if (cookieJoiner == null) {
                    cookieJoiner = new StringJoiner(COOKIE_SEPARATOR);
                }
                COOKIE_SPLITTER.split(value).forEach(cookieJoiner::add);
            } else {
                out.add(aName, convertHeaderValue(aName, value));
            }
        }

        if (cookieJoiner != null && cookieJoiner.length() != 0) {
            out.add(HttpHeaderNames.COOKIE, cookieJoiner.toString());
        }
    }

    private static CaseInsensitiveMap toLowercaseMap(Iterator<? extends CharSequence> valuesIter,
                                                     int arraySizeHint) {
        final CaseInsensitiveMap result = new CaseInsensitiveMap(arraySizeHint);

        while (valuesIter.hasNext()) {
            final AsciiString lowerCased = AsciiString.of(valuesIter.next()).toLowerCase();
            try {
                int index = lowerCased.forEachByte(FIND_COMMA);
                if (index != -1) {
                    int start = 0;
                    do {
                        result.add(lowerCased.subSequence(start, index, false).trim(), EMPTY_STRING);
                        start = index + 1;
                    } while (start < lowerCased.length() &&
                             (index = lowerCased.forEachByte(start,
                                                             lowerCased.length() - start, FIND_COMMA)) != -1);
                    result.add(lowerCased.subSequence(start, lowerCased.length(), false).trim(), EMPTY_STRING);
                } else {
                    result.add(lowerCased.trim(), EMPTY_STRING);
                }
            } catch (Exception e) {
                // This is not expect to happen because FIND_COMMA never throws but must be caught
                // because of the ByteProcessor interface.
                throw new IllegalStateException(e);
            }
        }
        return result;
    }

    /**
     * Filter the {@link HttpHeaderNames#TE} header according to the
     * special rules in the HTTP/2 RFC.
     *
     * @param entry the entry whose name is {@link HttpHeaderNames#TE}.
     * @param out the resulting HTTP/2 headers.
     */
    private static void toHttp2HeadersFilterTE(Entry<CharSequence, CharSequence> entry,
                                               HttpHeadersBuilder out) {
        if (AsciiString.indexOf(entry.getValue(), ',', 0) == -1) {
            if (AsciiString.contentEqualsIgnoreCase(AsciiString.trim(entry.getValue()),
                                                    HttpHeaderValues.TRAILERS)) {
                out.add(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS.toString());
            }
        } else {
            final List<CharSequence> teValues = StringUtil.unescapeCsvFields(entry.getValue());
            for (CharSequence teValue : teValues) {
                if (AsciiString.contentEqualsIgnoreCase(AsciiString.trim(teValue),
                                                        HttpHeaderValues.TRAILERS)) {
                    out.add(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS.toString());
                    break;
                }
            }
        }
    }

    /**
     * Converts the specified Armeria HTTP/2 {@link ResponseHeaders} into Netty HTTP/2 headers.
     *
     * @param inputHeaders the HTTP/2 response headers to convert.
     */
    public static Http2Headers toNettyHttp2ServerHeaders(HttpHeadersBuilder inputHeaders) {
        for (Entry<AsciiString, AsciiString> disallowed : HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST) {
            inputHeaders.remove(disallowed.getKey());
        }
        // TODO(ikhoon): Implement HttpHeadersBuilder.remove(Predicate<AsciiString>) to remove values
        //               with a predicate.
        for (AsciiString disallowed : disallowedResponseHeaderNames()) {
            inputHeaders.remove(disallowed);
        }
        return new ArmeriaHttp2Headers(inputHeaders);
    }

    /**
     * Converts the specified Armeria HTTP/2 response headers into Netty HTTP/2 headers.
     *
     * @param inputHeaders the HTTP/2 response headers to convert.
     */
    public static Http2Headers toNettyHttp2ServerTrailers(HttpHeaders inputHeaders) {
        final HttpHeadersBuilder builder = inputHeaders.toBuilder();

        for (Entry<AsciiString, AsciiString> disallowed : HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST) {
            builder.remove(disallowed.getKey());
        }
        for (AsciiString disallowed : PSEUDO_HEADERS) {
           builder.remove(disallowed);
        }
        for (Entry<AsciiString, AsciiString> disallowed : HTTP_TRAILER_DISALLOWED_LIST) {
            builder.remove(disallowed.getKey());
        }

        return new ArmeriaHttp2Headers(builder);
    }

    /**
     * Converts the specified Armeria HTTP/2 request headers into Netty HTTP/2 headers.
     *
     * @param inputHeaders the HTTP/2 request headers to convert.
     */
    public static Http2Headers toNettyHttp2ClientHeaders(HttpHeaders inputHeaders) {
        final int headerSizeHint = inputHeaders.size() + 3; // User_Agent, :scheme and :authority.
        final Http2Headers outputHeaders = new DefaultHttp2Headers(false, headerSizeHint);
        toNettyHttp2Client(inputHeaders, outputHeaders, false);
        return outputHeaders;
    }

    /**
     * Converts the specified Armeria HTTP/2 request headers into Netty HTTP/2 headers.
     *
     * @param inputHeaders the HTTP/2 request headers to convert.
     */
    public static Http2Headers toNettyHttp2ClientTrailers(HttpHeaders inputHeaders) {
        final int headerSizeHint = inputHeaders.size();
        final Http2Headers outputHeaders = new DefaultHttp2Headers(false, headerSizeHint);
        toNettyHttp2Client(inputHeaders, outputHeaders, true);
        return outputHeaders;
    }

    private static void toNettyHttp2Client(HttpHeaders inputHeaders, Http2Headers outputHeaders,
                                           boolean isTrailer) {
        for (Entry<AsciiString, String> entry : inputHeaders) {
            final AsciiString name = entry.getKey();
            final String value = entry.getValue();
            if (HTTP_TO_HTTP2_HEADER_DISALLOWED_LIST.contains(name)) {
                continue;
            }

            if (isTrailer && isTrailerDisallowed(name)) {
                continue;
            }

            outputHeaders.add(name, value);
        }

        if (!outputHeaders.contains(HttpHeaderNames.COOKIE)) {
            return;
        }

        // Split up cookies to allow for better compression.
        // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
        final List<CharSequence> cookies = outputHeaders.getAllAndRemove(HttpHeaderNames.COOKIE);
        for (CharSequence c : cookies) {
            outputHeaders.add(HttpHeaderNames.COOKIE, COOKIE_SPLITTER.split(c));
        }
    }

    /**
     * Translates and adds HTTP/2 response headers to HTTP/1.1 headers.
     *
     * @param inputHeaders the HTTP/2 response headers to convert.
     * @param outputHeaders the object which will contain the resulting HTTP/1.1 headers.
     */
    public static void toNettyHttp1ServerHeaders(
            HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
            Http1HeaderNaming http1HeaderNaming) {
        toNettyHttp1Server(inputHeaders, outputHeaders, http1HeaderNaming, false);
        HttpUtil.setKeepAlive(outputHeaders, HttpVersion.HTTP_1_1, true);
    }

    /**
     * Translates and adds HTTP/2 response trailers to HTTP/1.1 headers.
     *
     * @param inputHeaders The HTTP/2 response headers to convert.
     * @param outputHeaders The object which will contain the resulting HTTP/1.1 headers.
     */
    public static void toNettyHttp1ServerTrailers(
            HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
            Http1HeaderNaming http1HeaderNaming) {
        toNettyHttp1Server(inputHeaders, outputHeaders, http1HeaderNaming, true);
    }

    private static void toNettyHttp1Server(
            HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
            Http1HeaderNaming http1HeaderNaming, boolean isTrailer) {
        for (Entry<AsciiString, String> entry : inputHeaders) {
            final AsciiString name = entry.getKey();
            final String value = entry.getValue();
            if (HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.contains(name)) {
                continue;
            }

            if (isTrailer && isTrailerDisallowed(name)) {
                continue;
            }
            outputHeaders.add(http1HeaderNaming.convert(name), value);
        }
    }

    /**
     * Translates and adds HTTP/2 request headers to HTTP/1.1 headers.
     *
     * @param inputHeaders the HTTP/2 request headers to convert.
     * @param outputHeaders the object which will contain the resulting HTTP/1.1 headers.
     */
    public static void toNettyHttp1ClientHeaders(
            HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
            Http1HeaderNaming http1HeaderNaming) {
        toNettyHttp1Client(inputHeaders, outputHeaders, http1HeaderNaming, false);
        HttpUtil.setKeepAlive(outputHeaders, HttpVersion.HTTP_1_1, true);
    }

    /**
     * Translates and adds HTTP/2 request headers to HTTP/1.1 headers.
     *
     * @param inputHeaders the HTTP/2 request headers to convert.
     * @param outputHeaders the object which will contain the resulting HTTP/1.1 headers.
     */
    public static void toNettyHttp1ClientTrailers(
            HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
            Http1HeaderNaming http1HeaderNaming) {
        toNettyHttp1Client(inputHeaders, outputHeaders, http1HeaderNaming, true);
    }

    private static void toNettyHttp1Client(
            HttpHeaders inputHeaders, io.netty.handler.codec.http.HttpHeaders outputHeaders,
            Http1HeaderNaming http1HeaderNaming, boolean isTrailer) {
        StringJoiner cookieJoiner = null;

        for (Entry<AsciiString, String> entry : inputHeaders) {
            final AsciiString name = entry.getKey();
            final String value = entry.getValue();
            final AsciiString translatedName = REQUEST_HEADER_TRANSLATIONS.get(name);
            if (translatedName != null && !inputHeaders.contains(translatedName)) {
                outputHeaders.add(translatedName, value);
                continue;
            }

            if (HTTP2_TO_HTTP_HEADER_DISALLOWED_LIST.contains(name)) {
                continue;
            }

            if (isTrailer && isTrailerDisallowed(name)) {
                continue;
            }

            if (HttpHeaderNames.COOKIE.equals(name)) {
                // combine the cookie values into 1 header entry.
                // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
                if (cookieJoiner == null) {
                    cookieJoiner = new StringJoiner(COOKIE_SEPARATOR);
                }
                COOKIE_SPLITTER.split(value).forEach(cookieJoiner::add);
            } else {
                outputHeaders.add(http1HeaderNaming.convert(name), value);
            }
        }

        if (cookieJoiner != null && cookieJoiner.length() != 0) {
            outputHeaders.add(HttpHeaderNames.COOKIE, cookieJoiner.toString());
        }
    }

    /**
     * Returns a {@link ResponseHeaders} whose {@link HttpHeaderNames#CONTENT_LENGTH} is added or removed
     * according to the status of the specified {@code headers}, {@code content} and {@code trailers}.
     * The {@link HttpHeaderNames#CONTENT_LENGTH} is removed when:
     *

     *
the status of the specified {@code headers} is one of {@link HttpStatus#NO_CONTENT},
     *       {@link HttpStatus#RESET_CONTENT} or {@link HttpStatus#NOT_MODIFIED}

     *
the trailers exists

     *

     * The {@link HttpHeaderNames#CONTENT_LENGTH} is added when the state of the specified {@code headers}
     * does not meet the conditions above and {@link HttpHeaderNames#CONTENT_LENGTH} is not present
     * regardless of the fact that the content is empty or not.
     *
     * @throws IllegalArgumentException if the specified {@code content} is not empty when the specified
     *                                  {@link HttpStatus} is one of {@link HttpStatus#NO_CONTENT},
     *                                  {@link HttpStatus#RESET_CONTENT} and {@link HttpStatus#NOT_MODIFIED}.
     */
    public static ResponseHeaders setOrRemoveContentLength(ResponseHeaders headers, HttpData content,
                                                           HttpHeaders trailers) {
        requireNonNull(headers, "headers");
        requireNonNull(content, "content");
        requireNonNull(trailers, "trailers");

        final HttpStatus status = headers.status();

        if (isContentAlwaysEmptyWithValidation(status, content)) {
            if (status != HttpStatus.NOT_MODIFIED) {
                if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
                    final ResponseHeadersBuilder builder = headers.toBuilder();
                    builder.remove(HttpHeaderNames.CONTENT_LENGTH);
                    return builder.build();
                }
            } else {
                // 304 response can have the "content-length" header when it is a response to a conditional
                // GET request. See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
            }

            return headers;
        }

        if (!trailers.isEmpty()) {
            // Some of the client implementations such as "curl" ignores trailers if
            // the "content-length" header is present. We should not set "content-length" header when
            // trailers exists so that those clients can receive the trailers.
            // The response is sent using chunked transfer encoding in HTTP/1 or a DATA frame payload
            // in HTTP/2, so it's no worry.
            if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
                final ResponseHeadersBuilder builder = headers.toBuilder();
                builder.remove(HttpHeaderNames.CONTENT_LENGTH);
                return builder.build();
            }

            return headers;
        }

        if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) || !content.isEmpty()) {
            return headers.toBuilder()
                          .contentLength(content.length())
                          .build();
        }

        // The header contains "content-length" header and the content is empty.
        // Do not overwrite the header because a response to a HEAD request
        // will have no content even if it has non-zero content-length header.
        return headers;
    }

    public static String convertHeaderValue(AsciiString name, CharSequence value) {
        if (!(value instanceof AsciiString)) {
            return value.toString();
        }
        if (HEADER_VALUE_CACHE != null && CACHED_HEADERS.contains(name)) {
            final String converted = HEADER_VALUE_CACHE.get((AsciiString) value);
            assert converted != null; // loader does not return null.
            return converted;
        }
        return value.toString();
    }

    /**
     * Returns {@code true} if the specified header name is not allowed for HTTP trailers.
     */
    public static boolean isTrailerDisallowed(AsciiString name) {
        return HTTP_TRAILER_DISALLOWED_LIST.contains(name);
    }

    private static final class CaseInsensitiveMap
            extends DefaultHeaders<AsciiString, AsciiString, CaseInsensitiveMap> {

        CaseInsensitiveMap() {
            super(HTTP2_HEADER_NAME_HASHER, UnsupportedValueConverter.instance());
        }

        @SuppressWarnings("unchecked")
        CaseInsensitiveMap(int size) {
            super(HTTP2_HEADER_NAME_HASHER, UnsupportedValueConverter.instance(), NameValidator.NOT_NULL, size);
        }
    }

    /**
     * Returns a authority header value of specified host and port.
     */
    public static String authorityHeader(String host, int port, int defaultPort) {
        if (port == defaultPort) {
            return host;
        } else {
            final StringBuilder buf = new StringBuilder(host.length() + 6);
            buf.append(host);
            buf.append(':');
            buf.append(port);
            return buf.toString();
        }
    }

    /**
     * A 408 Request Timeout response can be received even without a request.
     * More details can be found at https://github.com/line/armeria/issues/3055.
     */
    public static boolean isRequestTimeoutResponse(HttpResponse httpResponse) {
        return httpResponse.status() == HttpResponseStatus.REQUEST_TIMEOUT &&
               "close".equalsIgnoreCase(httpResponse.headers().get(HttpHeaderNames.CONNECTION));
    }

    private ArmeriaHttpUtil() {}
}

## 内容2
### 文件路径：
core/src/main/java/com/linecorp/armeria/internal/common/PathAndQuery.java
### 代码内容：
package com.linecorp.armeria.internal.common;

import static io.netty.util.internal.StringUtil.decodeHexNibble;
import static java.util.Objects.requireNonNull;

import java.util.BitSet;
import java.util.Objects;
import java.util.Set;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.annotations.VisibleForTesting;

import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.metric.MeterIdPrefix;
import com.linecorp.armeria.internal.common.metric.CaffeineMetricSupport;

import io.micrometer.core.instrument.MeterRegistry;
import it.unimi.dsi.fastutil.Arrays;
import it.unimi.dsi.fastutil.bytes.ByteArrays;

/**
 * A parser of the raw path and query components of an HTTP path. Performs validation and allows caching of
 * results.
 */
public final class PathAndQuery {

    private static final PathAndQuery ROOT_PATH_QUERY = new PathAndQuery("/", null);

    private static final BitSet ALLOWED_PATH_CHARS = new BitSet();
    private static final BitSet ALLOWED_QUERY_CHARS = new BitSet();

    private static final int PERCENT_ENCODING_MARKER = 0xFF;

    private static final byte[] RAW_CHAR_TO_MARKER = new byte[256];
    private static final String[] MARKER_TO_PERCENT_ENCODED_CHAR = new String[256];

    static {
        final String allowedPathChars =
                "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=";
        for (int i = 0; i < allowedPathChars.length(); i++) {
            ALLOWED_PATH_CHARS.set(allowedPathChars.charAt(i));
        }

        final String allowedQueryChars =
                "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*,;=";
        for (int i = 0; i < allowedQueryChars.length(); i++) {
            ALLOWED_QUERY_CHARS.set(allowedQueryChars.charAt(i));
        }

        for (final ReservedChar reservedChar : ReservedChar.values()) {
            RAW_CHAR_TO_MARKER[reservedChar.rawChar] = reservedChar.marker;
            MARKER_TO_PERCENT_ENCODED_CHAR[reservedChar.marker] = reservedChar.percentEncodedChar;
        }
    }

    private static final Bytes EMPTY_QUERY = new Bytes(0);
    private static final Bytes ROOT_PATH = new Bytes(new byte[] { '/' });

    @Nullable
    private static final Cache<String, PathAndQuery> CACHE =
            Flags.parsedPathCacheSpec() != null ? buildCache(Flags.parsedPathCacheSpec()) : null;

    private static Cache<String, PathAndQuery> buildCache(String spec) {
        return Caffeine.from(spec).build();
    }

    public static void registerMetrics(MeterRegistry registry, MeterIdPrefix idPrefix) {
        if (CACHE != null) {
            CaffeineMetricSupport.setup(registry, idPrefix, CACHE);
        }
    }

    /**
     * Clears the currently cached parsed paths. Only for use in tests.
     */
    @VisibleForTesting
    public static void clearCachedPaths() {
        requireNonNull(CACHE, "CACHE");
        CACHE.asMap().clear();
    }

    /**
     * Returns paths that have had their parse result cached. Only for use in tests.
     */
    @VisibleForTesting
    public static Set<String> cachedPaths() {
        requireNonNull(CACHE, "CACHE");
        return CACHE.asMap().keySet();
    }

    /**
     * Validates the {@link String} that contains an absolute path and a query, and splits them into
     * the path part and the query part. If the path is usable (e.g., can be served a successful response from
     * the server and doesn't have variable path parameters), {@link PathAndQuery#storeInCache(String)} should
     * be called to cache the parsing result for faster future invocations.
     *
     * @return a {@link PathAndQuery} with the absolute path and query, or {@code null} if the specified
     *         {@link String} is not an absolute path or invalid.
     */
    @Nullable
    public static PathAndQuery parse(@Nullable String rawPath) {
        if (CACHE != null && rawPath != null) {
            final PathAndQuery parsed = CACHE.getIfPresent(rawPath);
            if (parsed != null) {
                return parsed;
            }
        }
        return splitPathAndQuery(rawPath);
    }

    /**
     * Stores this {@link PathAndQuery} into cache for the given raw path. This should be used by callers when
     * the parsed result was valid (e.g., when a server is able to successfully handle the parsed path).
     */
    public void storeInCache(@Nullable String rawPath) {
        if (CACHE != null && !cached && rawPath != null) {
            cached = true;
            CACHE.put(rawPath, this);
        }
    }

    private final String path;
    @Nullable
    private final String query;

    private boolean cached;

    private PathAndQuery(String path, @Nullable String query) {
        this.path = path;
        this.query = query;
    }

    public String path() {
        return path;
    }

    @Nullable
    public String query() {
        return query;
    }

    @Override
    public boolean equals(@Nullable Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof PathAndQuery)) {
            return false;
        }

        final PathAndQuery that = (PathAndQuery) o;
        return Objects.equals(path, that.path) &&
               Objects.equals(query, that.query);
    }

    @Override
    public int hashCode() {
        return Objects.hash(path, query);
    }

    @Override
    public String toString() {
        if (query == null) {
            return path;
        }
        return path + "?" + query;
    }

    @Nullable
    private static PathAndQuery splitPathAndQuery(@Nullable final String pathAndQuery) {
        final Bytes path;
        final Bytes query;

        if (pathAndQuery == null) {
            return ROOT_PATH_QUERY;
        }

        // Split by the first '?'.
        final int queryPos = pathAndQuery.indexOf('?');
        if (queryPos >= 0) {
            if ((path = decodePercentsAndEncodeToUtf8(
                    pathAndQuery, 0, queryPos, true)) == null) {
                return null;
            }
            if ((query = decodePercentsAndEncodeToUtf8(
                    pathAndQuery, queryPos + 1, pathAndQuery.length(), false)) == null) {
                return null;
            }
        } else {
            if ((path = decodePercentsAndEncodeToUtf8(
                    pathAndQuery, 0, pathAndQuery.length(), true)) == null) {
                return null;
            }
            query = null;
        }

        if (path.data[0] != '/') {
            // Do not accept a relative path.
            return null;
        }

        // Reject the prohibited patterns.
        if (pathContainsDoubleDots(path)) {
            return null;
        }

        return new PathAndQuery(encodeToPercents(path, true),
                                query != null ? encodeToPercents(query, false) : null);
    }

    @Nullable
    private static Bytes decodePercentsAndEncodeToUtf8(String value, int start, int end, boolean isPath) {
        final int length = end - start;
        if (length == 0) {
            return isPath ? ROOT_PATH : EMPTY_QUERY;
        }

        final Bytes buf = new Bytes(Math.max(length * 3 / 2, 4));
        boolean wasSlash = false;
        for (final CodePointIterator i = new CodePointIterator(value, start, end);
             i.hasNextCodePoint();/* noop */) {
            final int pos = i.position();
            final int cp = i.nextCodePoint();

            if (cp == '%') {
                final int hexEnd = pos + 3;
                if (hexEnd > end) {
                    // '%' or '%x' (must be followed by two hexadigits)
                    return null;
                }

                final int digit1 = decodeHexNibble(value.charAt(pos + 1));
                final int digit2 = decodeHexNibble(value.charAt(pos + 2));
                if (digit1 < 0 || digit2 < 0) {
                    // The first or second digit is not hexadecimal.
                    return null;
                }

                final int decoded = (digit1 << 4) | digit2;
                if (isPath) {
                    if (decoded == '/') {
                        // Do not decode '%2F' and '%2f' in the path to '/' for compatibility with
                        // other implementations in the ecosystem, e.g. HTTP/JSON to gRPC transcoding.
                        // https://github.com/googleapis/googleapis/blob/02710fa0ea5312d79d7fb986c9c9823fb41049a9/google/api/http.proto#L257-L258

                        // Insert a special mark so we can distinguish a raw character ('/') and
                        // percent-encoded character ('%2F') in a path string.
                        // We will encode this mark back into a percent-encoded character later.
                        final byte marker = RAW_CHAR_TO_MARKER['/'];
                        buf.ensure(2);
                        buf.add((byte) PERCENT_ENCODING_MARKER);
                        buf.add(marker);
                        wasSlash = false;
                    } else {
                        if (appendOneByte(buf, decoded, wasSlash, isPath)) {
                            wasSlash = false;
                        } else {
                            return null;
                        }
                    }
                } else {
                    // If query:
                    final byte marker = RAW_CHAR_TO_MARKER[decoded];
                    if (marker != 0) {
                        // Insert a special mark so we can distinguish a raw character and percent-encoded
                        // character in a query string, such as '&' and '%26'.
                        // We will encode this mark back into a percent-encoded character later.
                        buf.ensure(2);
                        buf.add((byte) PERCENT_ENCODING_MARKER);
                        buf.add(marker);
                        wasSlash = false;
                    } else if (appendOneByte(buf, decoded, wasSlash, isPath)) {
                        wasSlash = decoded == '/';
                    } else {
                        return null;
                    }
                }

                i.position(hexEnd);
                continue;
            }

            if (cp == '+' && !isPath) {
                buf.ensure(1);
                buf.add((byte) ' ');
                wasSlash = false;
                continue;
            }

            if (cp <= 0x7F) {
                if (!appendOneByte(buf, cp, wasSlash, isPath)) {
                    return null;
                }
                wasSlash = cp == '/';
                continue;
            }

            if (cp <= 0x7ff) {
                buf.ensure(2);
                buf.add((byte) ((cp >>> 6) | 0b110_00000));
                buf.add((byte) (cp & 0b111111 | 0b10_000000));
            } else if (cp <= 0xffff) {
                buf.ensure(3);
                buf.add((byte) ((cp >>> 12) | 0b1110_0000));
                buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));
                buf.add((byte) ((cp & 0b111111) | 0b10_000000));
            } else if (cp <= 0x1fffff) {
                buf.ensure(4);
                buf.add((byte) ((cp >>> 18) | 0b11110_000));
                buf.add((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));
                buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));
                buf.add((byte) ((cp & 0b111111) | 0b10_000000));
            } else if (cp <= 0x3ffffff) {
                // A valid unicode character will never reach here, but for completeness.
                // http://unicode.org/mail-arch/unicode-ml/Archives-Old/UML018/0330.html
                buf.ensure(5);
                buf.add((byte) ((cp >>> 24) | 0b111110_00));
                buf.add((byte) (((cp >>> 18) & 0b111111) | 0b10_000000));
                buf.add((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));
                buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));
                buf.add((byte) ((cp & 0b111111) | 0b10_000000));
            } else {
                // A valid unicode character will never reach here, but for completeness.
                // http://unicode.org/mail-arch/unicode-ml/Archives-Old/UML018/0330.html
                buf.ensure(6);
                buf.add((byte) ((cp >>> 30) | 0b1111110_0));
                buf.add((byte) (((cp >>> 24) & 0b111111) | 0b10_000000));
                buf.add((byte) (((cp >>> 18) & 0b111111) | 0b10_000000));
                buf.add((byte) (((cp >>> 12) & 0b111111) | 0b10_000000));
                buf.add((byte) (((cp >>> 6) & 0b111111) | 0b10_000000));
                buf.add((byte) ((cp & 0b111111) | 0b10_000000));
            }

            wasSlash = false;
        }

        return buf;
    }

    private static boolean appendOneByte(Bytes buf, int cp, boolean wasSlash, boolean isPath) {
        if (cp == 0x7F) {
            // Reject the control character: 0x7F
            return false;
        }

        if (cp >>> 5 == 0) {
            // Reject the control characters: 0x00..0x1F
            if (isPath) {
                return false;
            } else if (cp != 0x0A && cp != 0x0D && cp != 0x09) {
                // .. except 0x0A (LF), 0x0D (CR) and 0x09 (TAB) because they are used in a form.
                return false;
            }
        }

        if (cp == '/' && isPath) {
            if (!wasSlash) {
                buf.ensure(1);
                buf.add((byte) '/');
            } else {
                // Remove the consecutive slashes: '/path//with///consecutive////slashes'.
            }
        } else {
            buf.ensure(1);
            buf.add((byte) cp);
        }

        return true;
    }

    private static boolean pathContainsDoubleDots(Bytes path) {
        final int length = path.length;
        byte b0 = 0;
        byte b1 = 0;
        byte b2 = '/';
        for (int i = 1; i < length; i++) {
            final byte b3 = path.data[i];
            if (b3 == '/' && b2 == '.' && b1 == '.' && b0 == '/') {
                return true;
            }
            b0 = b1;
            b1 = b2;
            b2 = b3;
        }

        return b0 == '/' && b1 == '.' && b2 == '.';
    }

    private static String encodeToPercents(Bytes value, boolean isPath) {
        final BitSet allowedChars = isPath ? ALLOWED_PATH_CHARS : ALLOWED_QUERY_CHARS;
        final int length = value.length;
        boolean needsEncoding = false;
        for (int i = 0; i < length; i++) {
            if (!allowedChars.get(value.data[i] & 0xFF)) {
                needsEncoding = true;
                break;
            }
        }

        if (!needsEncoding) {
            // Deprecated, but it fits perfect for our use case.
            // noinspection deprecation
            return new String(value.data, 0, 0, length);
        }

        final StringBuilder buf = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            final int b = value.data[i] & 0xFF;

            if (b == PERCENT_ENCODING_MARKER && (i + 1) < length) {
                final int marker = value.data[i + 1] & 0xFF;
                final String percentEncodedChar = MARKER_TO_PERCENT_ENCODED_CHAR[marker];
                if (percentEncodedChar != null) {
                    buf.append(percentEncodedChar);
                    i++;
                    continue;
                }
            }

            if (allowedChars.get(b)) {
                buf.append((char) b);
            } else if (b == ' ') {
                if (isPath) {
                    buf.append("%20");
                } else {
                    buf.append('+');
                }
            } else {
                buf.append('%');
                appendHexNibble(buf, b >>> 4);
                appendHexNibble(buf, b & 0xF);
            }
        }

        return buf.toString();
    }

    private static void appendHexNibble(StringBuilder buf, int nibble) {
        if (nibble < 10) {
            buf.append((char) ('0' + nibble));
        } else {
            buf.append((char) ('A' + nibble - 10));
        }
    }

    private static final class Bytes {
        byte[] data;
        int length;

        Bytes(int initialCapacity) {
            data = new byte[initialCapacity];
        }

        Bytes(byte[] data) {
            this.data = data;
            length = data.length;
        }

        void add(byte b) {
            data[length++] = b;
        }

        void ensure(int numBytes) {
            int newCapacity = length + numBytes;
            if (newCapacity <= data.length) {
                return;
            }

            newCapacity =
                    (int) Math.max(Math.min((long) data.length + (data.length >> 1), Arrays.MAX_ARRAY_SIZE),
                                   newCapacity);

            data = ByteArrays.forceCapacity(data, newCapacity, length);
        }
    }

    private static final class CodePointIterator {
        private final CharSequence str;
        private final int end;
        private int pos;

        CodePointIterator(CharSequence str, int start, int end) {
            this.str = str;
            this.end = end;
            pos = start;
        }

        int position() {
            return pos;
        }

        void position(int pos) {
            this.pos = pos;
        }

        boolean hasNextCodePoint() {
            return pos < end;
        }

        int nextCodePoint() {
            assert pos < end;

            final char c1 = str.charAt(pos++);
            if (Character.isHighSurrogate(c1) && pos < end) {
                final char c2 = str.charAt(pos);
                if (Character.isLowSurrogate(c2)) {
                    pos++;
                    return Character.toCodePoint(c1, c2);
                }
            }

            return c1;
        }
    }

    /**
     * Reserved characters which require percent-encoding. These values are only used for constructing
     * {@link #RAW_CHAR_TO_MARKER} and {@link #MARKER_TO_PERCENT_ENCODED_CHAR} mapping tables.
     *
     * @see RFC 3986, section 2.2
     */
    private enum ReservedChar {
        GEN_DELIM_01(':', "%3A", (byte) 0x01),
        GEN_DELIM_02('/', "%2F", (byte) 0x02),
        GEN_DELIM_03('?', "%3F", (byte) 0x03),
        GEN_DELIM_04('#', "%23", (byte) 0x04),
        GEN_DELIM_05('[', "%5B", (byte) 0x05),
        GEN_DELIM_06(']', "%5D", (byte) 0x06),
        GEN_DELIM_07('@', "%40", (byte) 0x07),

        SUB_DELIM_01('!', "%21", (byte) 0x11),
        SUB_DELIM_02('$', "%24", (byte) 0x12),
        SUB_DELIM_03('&', "%26", (byte) 0x13),
        SUB_DELIM_04('\'', "%27", (byte) 0x14),
        SUB_DELIM_05('(', "%28", (byte) 0x15),
        SUB_DELIM_06(')', "%29", (byte) 0x16),
        SUB_DELIM_07('*', "%2A", (byte) 0x17),
        SUB_DELIM_08('+', "%2B", (byte) 0x18),
        SUB_DELIM_09(',', "%2C", (byte) 0x19),
        SUB_DELIM_10(';', "%3B", (byte) 0x1A),
        SUB_DELIM_11('=', "%3D", (byte) 0x1B);

        private final int rawChar;
        private final String percentEncodedChar;
        private final byte marker;

        ReservedChar(int rawChar, String percentEncodedChar, byte marker) {
            this.rawChar = rawChar;
            this.percentEncodedChar = percentEncodedChar;
            this.marker = marker;
        }
    }
}

## 内容3
### 文件路径：
core/src/main/java/com/linecorp/armeria/server/RoutingResultBuilder.java
### 代码内容：
package com.linecorp.armeria.server;

import static com.google.common.base.Preconditions.checkArgument;
import static com.linecorp.armeria.server.RoutingResult.LOWEST_SCORE;
import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableMap;

import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.internal.common.ArmeriaHttpUtil;

/**
 * Builds a new {@link RoutingResult}.
 */
public final class RoutingResultBuilder {

    private RoutingResultType type = RoutingResultType.MATCHED;

    @Nullable
    private String path;

    @Nullable
    private String query;

    @Nullable
    private ImmutableMap.Builder<String, String> pathParams;

    private int score = LOWEST_SCORE;

    @Nullable
    private MediaType negotiatedResponseMediaType;

    RoutingResultBuilder() {}

    RoutingResultBuilder(int expectedNumParams) {
        pathParams = ImmutableMap.builderWithExpectedSize(expectedNumParams);
    }

    /**
     * Sets the result type.
     *
     * @param type {@link RoutingResultType#MATCHED} or {@link RoutingResultType#CORS_PREFLIGHT}.
     */
    public RoutingResultBuilder type(RoutingResultType type) {
        requireNonNull(type, "type");
        checkArgument(type != RoutingResultType.NOT_MATCHED,
                      "type: %s (expected: %s or %s)",
                      type, RoutingResultType.MATCHED, RoutingResultType.CORS_PREFLIGHT);
        this.type = type;
        return this;
    }

    /**
     * Sets the mapped path, encoded as defined in RFC3986.
     */
    public RoutingResultBuilder path(String path) {
        this.path = requireNonNull(path, "path");
        return this;
    }

    /**
     * Sets the specified query.
     */
    public RoutingResultBuilder query(@Nullable String query) {
        this.query = query;
        return this;
    }

    /**
     * Adds a decoded path parameter.
     */
    public RoutingResultBuilder decodedParam(String name, String value) {
        pathParams().put(requireNonNull(name, "name"), requireNonNull(value, "value"));
        return this;
    }

    /**
     * Adds an encoded path parameter, which will be decoded in UTF-8 automatically.
     */
    public RoutingResultBuilder rawParam(String name, String value) {
        pathParams().put(requireNonNull(name, "name"),
                         ArmeriaHttpUtil.decodePath(requireNonNull(value, "value")));
        return this;
    }

    /**
     * Sets the score.
     */
    public RoutingResultBuilder score(int score) {
        this.score = score;
        return this;
    }

    /**
     * Sets the negotiated producible {@link MediaType}.
     */
    public RoutingResultBuilder negotiatedResponseMediaType(MediaType negotiatedResponseMediaType) {
        this.negotiatedResponseMediaType = requireNonNull(negotiatedResponseMediaType,
                                                          "negotiatedResponseMediaType");
        return this;
    }

    /**
     * Returns a newly-created {@link RoutingResult}.
     */
    public RoutingResult build() {
        if (path == null) {
            return RoutingResult.empty();
        }

        return new RoutingResult(type, path, query,
                                 pathParams != null ? pathParams.build() : ImmutableMap.of(),
                                 score, negotiatedResponseMediaType);
    }

    private ImmutableMap.Builder<String, String> pathParams() {
        if (pathParams != null) {
            return pathParams;
        }
        return pathParams = ImmutableMap.builder();
    }
}

Output:
【漏洞类型】路径遍历漏洞
【严重性】High
【漏洞摘要】此代码在处理HTTP路径时存在路径遍历漏洞。具体表现为`PathAndQuery.splitPathAndQuery`函数中的`pathContainsDoubleDots`方法，它没有正确处理路径中的百分号编码字符（如`%2F`），从而可能允许攻击者通过构造特殊的路径来访问受限的文件或目录。
【关键代码模式】
- `PathAndQuery.splitPathAndQuery`函数
- `PathAndQuery.pathContainsDoubleDots`方法
- `ArmeriaHttpUtil.decodePath`函数
【修复方法说明】修复方法通过在`PathAndQuery.decodePercentsAndEncodeToUtf8`中添加对`%2F`的处理，确保它不会被意外解码为普通斜杠，从而防止路径遍历漏洞。同时，将`ArmeriaHttpUtil.decodePath`拆分为`decodePath`和`decodePathParam`，并在`RoutingResultBuilder.rawParam`中使用`decodePathParam`，以确保路径参数中的`%2F`被正确解码。