All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.mslinks.ShellLinkHelper.kt Maven / Gradle / Ivy

Go to download

ServerPackCreators API, to create server packs from Forge, Fabric, Quilt, LegacyFabric and NeoForge modpacks.

The newest version!
/*
	https://github.com/BlackOverlord666/mslinks
	
	Copyright (c) 2015 Dmitrii Shamrikov

	Licensed under the WTFPL
	You may obtain a copy of the License at
 
	http://www.wtfpl.net/about/
 
	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*/
package mslinks

import mslinks.data.*
import java.io.IOException
import java.nio.file.Files
import java.nio.file.InvalidPathException
import java.nio.file.Paths
import java.util.regex.Pattern

/**
 * Helper class to manipulate ShellLink properties in batches for common tasks
 * ShellLink can be used directly without helper for more detailed set up
 */
@Suppress("unused")
class ShellLinkHelper(var link: ShellLink) {
    enum class Options {
        None,
        ForceTypeDirectory,
        ForceTypeFile
    }

    @Suppress("RegExpSingleCharAlternation")
    private val backForwardSlashRegex = "^(\\\\|/)".toRegex()
    private val backSlash = "\\"
    private val backSlashChar = '\\'
    private val backSlashBackSlash = "\\\\"


    /**
     * Sets LAN target path
     * @param path is an absolute in the form '\\host\share\path\to\target'
     * @throws ShellLinkException
     */
    @Throws(ShellLinkException::class)
    fun setNetworkTarget(path: String): ShellLinkHelper {
        return setNetworkTarget(path, Options.None)
    }

    /**
     * Sets LAN target path
     * @param path is an absolute in the form '\\host\share\path\to\target'
     * @throws ShellLinkException
     */
    @Throws(ShellLinkException::class)
    fun setNetworkTarget(path: String, options: Options): ShellLinkHelper {
        var tempPath = path
        if (!tempPath.startsWith(backSlash)) {
            tempPath = backSlash + tempPath
        }
        if (!tempPath.startsWith(backSlashBackSlash)) {
            tempPath = backSlash + tempPath
        }
        val p1 = tempPath.indexOf(backSlashChar, 2) // hostname
        val p2 = tempPath.indexOf(backSlashChar, p1 + 1) // share name
        if (p1 != -1) {
            val info = if (link.header!!.linkFlags.hasLinkInfo()) {
                link.linkInfo
            } else {
                link.createLinkInfo()
            }
            if (p2 != -1) {
                info!!.createCommonNetworkRelativeLink().setNetName(tempPath.substring(0, p2))
                info.setCommonPathSuffix(tempPath.substring(p2 + 1))
            } else {
                info!!.createCommonNetworkRelativeLink().setNetName(tempPath)
                info.setCommonPathSuffix("")
            }
            link.header!!.fileAttributesFlags.setDirectory()
            val forceFile = options == Options.ForceTypeFile
            val forceDirectory = options == Options.ForceTypeDirectory
            if (forceFile
                || !forceDirectory
                && Files.isRegularFile(Paths.get(tempPath))
            ) {
                link.header!!.fileAttributesFlags.clearDirectory()
            }
        } else {
            link.header!!.fileAttributesFlags.clearDirectory()
        }
        link.header!!.linkFlags.setHasExpString()
        link.environmentVariable!!.setVariable(tempPath)
        return this
    }

    /**
     * Sets target on local computer, e.g. "C:\path\to\target"
     * @param drive is a letter part of the path, e.g. "C" or "D"
     * @param absolutePath is a path in the specified drive, e.g. "path\to\target"
     * @throws ShellLinkException
     */
    @Throws(ShellLinkException::class)
    fun setLocalTarget(drive: String?, absolutePath: String): ShellLinkHelper {
        return setLocalTarget(drive, absolutePath, Options.None)
    }

    /**
     * Sets target on local computer, e.g. "C:\path\to\target"
     * @param drive is a letter part of the path, e.g. "C" or "D"
     * @param absolutePath is a path in the specified drive, e.g. "path\to\target"
     * @throws ShellLinkException
     */
    @Throws(ShellLinkException::class)
    fun setLocalTarget(drive: String?, absolutePath: String, options: Options): ShellLinkHelper {
        var tempAbsolutePath = absolutePath
        link.header!!.linkFlags.setHasLinkTargetIDList()
        val idList = link.createTargetIdList()
        // root is computer
        idList.add(ItemIDRoot().setClsid(Registry.CLSID_COMPUTER))

        // drive
        // windows usually creates TYPE_DRIVE_MISC here but TYPE_DRIVE_FIXED also works fine
        val driveItem = ItemIDDrive(ItemID.TYPE_DRIVE_MISC).setName(drive)
        idList.add(driveItem)

        // each segment of the path is directory
        tempAbsolutePath = tempAbsolutePath.replace(backForwardSlashRegex, "")
        val absoluteTargetPath = driveItem.name + tempAbsolutePath
        val path = tempAbsolutePath.split(backForwardSlashRegex).dropLastWhile { it.isEmpty() }.toTypedArray()
        for (i in path) {
            idList.add(ItemIDFS(ItemID.TYPE_FS_DIRECTORY).setName(i))
        }
        val info = if (link.header!!.linkFlags.hasLinkInfo()) {
            link.linkInfo
        } else {
            link.createLinkInfo()
        }
        info!!.createVolumeID().setDriveType(VolumeID.DRIVE_FIXED)
        info.setLocalBasePath(absoluteTargetPath)
        link.header!!.fileAttributesFlags.setDirectory()
        val forceFile = options == Options.ForceTypeFile
        val forceDirectory = options == Options.ForceTypeDirectory
        if (forceFile || !forceDirectory && Files.isRegularFile(Paths.get(absoluteTargetPath))) {
            link.header!!.fileAttributesFlags.clearDirectory()
            idList.last!!.setTypeFlags(ItemID.TYPE_FS_FILE)
        }
        return this
    }

    /**
     * Sets target relative to a special folder defined by a GUID.
     * Use [Registry] class to get an available GUID by name or predefined constants.
     * Note that you can add your own GUIDs available on your system
     * @param root a GUID defining a special folder, e.g. Registry.CLSID_DOCUMENTS. Must be registered in the [Registry]
     * @param path a path relative to the special folder, e.g. "path\to\target"
     * @throws ShellLinkException
     */
    @Throws(ShellLinkException::class)
    fun setSpecialFolderTarget(root: GUID?, path: String, options: Options): ShellLinkHelper {
        if (options != Options.ForceTypeFile && options != Options.ForceTypeDirectory) {
            throw ShellLinkException("The type of target is not specified. You have to specify whether it is a file or a directory.")
        }
        link.header!!.linkFlags.setHasLinkTargetIDList()
        val idList = link.createTargetIdList()
        // although later systems use ItemIDRoot(computer) + ItemIDRegFolder(root clsid) pair, always set root clsid as ItemIDRoot for simplicity
        idList.add(ItemIDRoot().setClsid(root))

        // each segment of the path is directory
        pathDirectory(path, idList, options)
        return this
    }

    private fun pathDirectory(path: String, idList: LinkTargetIDList, options: Options) {
        val tempPath = path.replace(backForwardSlashRegex, "")
        val pathSegments = tempPath.split(backForwardSlashRegex).dropLastWhile { it.isEmpty() }.toTypedArray()
        for (i in pathSegments) {
            idList.add(ItemIDFS(ItemID.TYPE_FS_DIRECTORY).setName(i))
        }
        link.header!!.fileAttributesFlags.setDirectory()
        if (options == Options.ForceTypeFile) {
            link.header!!.fileAttributesFlags.clearDirectory()
            idList.last!!.setTypeFlags(ItemID.TYPE_FS_FILE)
        }
    }

    /**
     * Sets target relative to desktop directory of the user opening the link. This method is universal
     * because it works without Registry.CLSID_DESKTOP which is available only on later systems
     * @param path a path relative to the desktop, e.g. "path\to\target"
     * @throws ShellLinkException
     */
    @Throws(ShellLinkException::class)
    fun setDesktopRelativeTarget(path: String, options: Options): ShellLinkHelper {
        if (options != Options.ForceTypeFile && options != Options.ForceTypeDirectory) {
            throw ShellLinkException("The type of target is not specified. You have to specify whether it is a file or a directory.")
        }
        link.header!!.linkFlags.setHasLinkTargetIDList()
        val idList = link.createTargetIdList()

        // no root item here

        // each segment of the path is directory
        pathDirectory(path, idList, options)
        return this
    }

    /**
     * Serializes `ShellLink` to specified `path`. Sets appropriate relative path
     * and working directory if possible and if they are not already set
     */
    @Throws(IOException::class)
    fun saveTo(path: String?): ShellLinkHelper {
        val savingPath = Paths.get(path!!).toAbsolutePath().normalize()
        if (Files.isDirectory(savingPath)) {
            throw IOException("can't save ShellLink to \"$savingPath\" because there is a directory with this name")
        }
        link.setLinkFileSource(savingPath)
        val savingDir = savingPath.parent
        try {
            val target = Paths.get(link.resolveTarget()!!)
            if (!link.header!!.linkFlags.hasRelativePath()) {
                // this will always be false on linux
                if (savingDir.root == target.root) {
                    link.setRelativePath(savingDir.relativize(target).toString())
                }
            }
            if (!link.header!!.linkFlags.hasWorkingDir()) {
                // this will always be false on linux
                if (Files.isRegularFile(target)) {
                    link.setWorkingDir(target.parent.toString())
                }
            }
        } catch (e: InvalidPathException) {
            // skip automatic relative path and working dir if path is some special folder
        }
        Files.createDirectories(savingDir)
        link.serialize(Files.newOutputStream(savingPath))
        return this
    }

    companion object {
        private const val BACKSLASH = "\\"
        private const val BACKSLASH_BACKSLASH = "\\\\"

        /**
         * Universal all-by-default creation of the link
         * @param target - absolute path for the target file in windows format (e.g. C:\path\to\file.txt)
         * @param linkPath - where to save link file
         * @return
         * @throws IOException
         * @throws ShellLinkException
         */
        @Throws(IOException::class, ShellLinkException::class)
        fun createLink(target: String, linkPath: String?): ShellLinkHelper {
            var tempTarget = target
            tempTarget = resolveEnvVariables(tempTarget)
            val helper = ShellLinkHelper(ShellLink())
            if (tempTarget.startsWith(BACKSLASH_BACKSLASH)) {
                helper.setNetworkTarget(tempTarget)
            } else {
                val parts = tempTarget.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
                if (parts.size != 2) throw ShellLinkException("Wrong path '$tempTarget'")
                helper.setLocalTarget(parts[0], parts[1])
            }
            helper.saveTo(linkPath)
            return helper
        }

        fun resolveEnvVariables(path: String): String {
            var tempPath = path
            for ((key, value) in env) {
                val p = Pattern.quote(key)
                val r = value.replace(BACKSLASH, BACKSLASH_BACKSLASH)
                tempPath = Pattern.compile("%$p%", Pattern.CASE_INSENSITIVE).matcher(tempPath).replaceAll(r)
            }
            return tempPath
        }

        private val env = System.getenv()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy