vorteil/vorteil

View on GitHub
pkg/vproj/import-so.go

Summary

Maintainability
A
3 hrs
Test Coverage
C
76%
package vproj

/**
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2020 vorteil.io Pty Ltd
 */

import (
    "debug/elf"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "path/filepath"
    "strings"

    "github.com/vorteil/vorteil/pkg/elog"
)

const (
    dynamicLinkerConfig     = "/etc/ld.so.conf"
    defaultLinuxUserLibPath = "/usr/lib"
    defaultLinuxLibPath     = "/lib"
)

var defaultLibs = []string{"libnss_dns.so.2", "libnss_files.so.2", "libresolv.so.2"}

// NewImportSharedObject : This function is used to create and initialize a importSharedObjectsOperation.
//     This function requires three args:
//            projectPath: The target project you wish to scan, and import shared objects to.
//            excludeDefaultLibs: Whether or not to exclude default libraries.
//            logger: logger object to log with
//     Once initialized a importSharedObjectsOperation object will be returned.
//     Running importSharedObjectsOperation.Start() will then begin the operation.
func NewImportSharedObject(projectPath string, excludeDefaultLibs bool, logger elog.View) (*importSharedObjectsOperation, error) {
    var isoOperation importSharedObjectsOperation

    if fi, err := os.Stat(projectPath); os.IsNotExist(err) {
        return nil, fmt.Errorf("'%s' path does not exist", projectPath)
    } else if !fi.IsDir() {
        return nil, fmt.Errorf("'%s' path is not a directory", projectPath)
    }

    isoOperation.projectDir = projectPath
    isoOperation.excludeDefaultLibs = excludeDefaultLibs
    isoOperation.logger = logger
    isoOperation.sharedObjects = make(map[string]string)

    if err := isoOperation.initLDPATHS(); err != nil {
        return nil, err
    }

    return &isoOperation, nil
}

type importSharedObjectsOperation struct {
    projectDir string

    sharedObjects map[string]string // Tracks shared objects, if value is "", shared object is missing from system

    excludeDefaultLibs  bool
    sharedObjectClasses []elf.Class
    imported32bit       bool
    imported64bit       bool

    ldPATHS []string // SYSTEM LD PATHS

    logger elog.View
}

//getLDPathsFromENV: Gets LD_LIBRARY_PATH env value and split the paths into a string slice
func getLDPathsFromENV() []string {
    var paths = make([]string, 0, 0)
    y := os.Getenv("LD_LIBRARY_PATH")
    if y != "" {
        x := filepath.SplitList(y)
        paths = append(paths, x...)
    }

    return paths
}

//loadLDPathsFromLinkerConfig Load LD config file located at parameter 1 'path', scan for library paths
//    and append these paths to importSharedObjectsOperation tracked paths at 'ldPATHS.
func (isoOp *importSharedObjectsOperation) loadLDPathsFromLinkerConfig(path string) error {
    if strings.Contains(path, "*") {
        return nil
    }
    data, err := ioutil.ReadFile(path)
    if err != nil {
        return err
    }
    lines := strings.Split(string(data), "\n")
    for _, line := range lines {
        if strings.HasPrefix(line, "/") {
            isoOp.ldPATHS = append(isoOp.ldPATHS, line)
            continue
        }

        if strings.HasPrefix(line, "include") {
            line = filepath.ToSlash(strings.TrimSpace(strings.TrimPrefix(line, "include ")))
            matches, err := filepath.Glob(line)
            if err != nil {
                return err
            }

            for _, match := range matches {
                err = isoOp.loadLDPathsFromLinkerConfig(match)
                if err != nil {
                    return err
                }
            }
            err = isoOp.loadLDPathsFromLinkerConfig(line)
            if err != nil {
                return err
            }
        }
        if strings.TrimSpace(line) != "" {
            if strings.HasPrefix(line, "#") || strings.Contains(line, "*") {
                continue
            }
            return fmt.Errorf("unexpected line in '%s' file: %s", dynamicLinkerConfig, line)
        }
    }
    return nil
}

//initLDPATHS: initializes importSharedObjectsOperation.ldPATHS so that it may be used later with findLib func
func (isoOp *importSharedObjectsOperation) initLDPATHS() error {
    // Load paths from env
    isoOp.ldPATHS = getLDPathsFromENV()
    // Load paths from linker config
    if err := isoOp.loadLDPathsFromLinkerConfig(dynamicLinkerConfig); err != nil {
        return err
    }
    // Append Common Linux Lib Paths
    isoOp.ldPATHS = append(isoOp.ldPATHS, defaultLinuxLibPath)
    isoOp.ldPATHS = append(isoOp.ldPATHS, defaultLinuxUserLibPath)
    return nil
}

func (isoOp *importSharedObjectsOperation) copyDefaultLibs() error {
    classesImported := make([]elf.Class, 0)

    if isoOp.imported32bit {
        classesImported = append(classesImported, elf.ELFCLASS32)
    }
    if isoOp.imported64bit {
        classesImported = append(classesImported, elf.ELFCLASS64)
    }

    isoOp.logger.Infof("including Default Libs")
    for i := range classesImported {
        for j := range defaultLibs {
            elfLibPath, found, err := isoOp.findLib(defaultLibs[j], classesImported[i])
            if err == nil && found {
                isoOp.sharedObjects[defaultLibs[j]] = elfLibPath
                err := isoOp.addSharedObjects(elfLibPath)
                if err != nil {
                    return err
                }
            } else if err == nil {
                isoOp.sharedObjects[defaultLibs[j]] = elfLibPath
            } else {
                return err
            }
        }
    }

    return nil
}
func (isoOp *importSharedObjectsOperation) Start() error {
    var err error
    var projectPaths []string

    isoProgress := isoOp.logger.NewProgress("Importing Shared Objects ", "", 0)
    defer isoProgress.Finish(true)

    // Get all a path to all files in project dir
    projectPaths, err = isoOp.getProjectFiles()
    if err != nil {
        return err
    }

    // Find Import Libraries of project files and add them to map
    for i := range projectPaths {
        isoOp.addSharedObjects(projectPaths[i])
    }

    // Find Import Libraries of default files and add them to map
    if !isoOp.excludeDefaultLibs {
        err = isoOp.copyDefaultLibs()
        if err != nil {
            return err
        }
    }

    err = isoOp.copySharedObjects()
    if err == nil {
        isoOp.logger.Printf("Completed.")
    }

    return err
}

// copySharedObjects: loops over stored sharedObjects in operations map and copies those
//    shared objects into project director
func (isoOp *importSharedObjectsOperation) copySharedObjects() error {
    for so, soPath := range isoOp.sharedObjects {
        soProjectPath := filepath.Join(isoOp.projectDir, soPath)
        if soPath == "" {
            // Unfound Shared Object
            isoOp.logger.Warnf("shared object '%s' could not be found, so has been skipped", so)
        } else {
            if copied, err := isoOp.copyLib(soPath, soProjectPath); !copied {
                isoOp.logger.Debugf("skipping '%s' already exists", so)
            } else if err != nil {
                isoOp.logger.Errorf("failed to copy '%s'", so)
                return err
            }
        }
    }

    return nil
}

// copyLib: Copies file at libPath to destPath if it does not exists.
//    If destPath parent dir does not exists it is created.
//    If libPath is a symlink, evaluate that symlink and create a symlink to target at destPath
func (isoOp *importSharedObjectsOperation) copyLib(libPath, destPath string) (bool, error) {
    destPath = isoOp.adjustPath(destPath)
    isoOp.logger.Infof("copying '%s' > '%s", libPath, destPath)

    // evaluate libPath so see if its a symlink
    realLibPath, err := filepath.EvalSymlinks(libPath)
    if err != nil {
        panic(err)
    }

    // If target link has same name, inherit real path
    if filepath.Base(libPath) == filepath.Base(realLibPath) {
        libPath = realLibPath
    }

    // Check if path exists
    if _, err := os.Stat(destPath); os.IsNotExist(err) {
        err := os.MkdirAll(filepath.Dir(destPath), 0777)
        if err != nil {
            return false, err
        }

        // If true libPath is a symlink
        if realLibPath != libPath {
            isoOp.logger.Debugf("found symlink %s > %s", destPath, realLibPath)
            err := os.Symlink(strings.TrimPrefix(realLibPath, "/usr"), destPath)
            if err != nil {
                return false, err
            }

            // Recall copyLib to copy symlinks target into projectDir
            return isoOp.copyLib(realLibPath, filepath.Join(isoOp.projectDir, realLibPath))
        } else {
            // libPath is not a symlink, Copy file over
            f, err := os.Create(destPath)
            if err != nil {
                return false, err
            }
            defer f.Close()

            src, err := os.Open(libPath)
            if err != nil {
                return false, err
            }
            defer src.Close()

            _, err = io.Copy(f, src)
            if err != nil {
                return false, err
            }
        }
    } else {
        return false, nil
    }

    return true, nil
}

//adjustPath: If /usr is the top level directory of the path, trim it out.
//    Readjust path of linker to /lib64
func (isoOp *importSharedObjectsOperation) adjustPath(path string) string {
    adjustedPrefix := filepath.Join(isoOp.projectDir, "/usr")
    if strings.HasPrefix(path, adjustedPrefix) {
        path = strings.TrimPrefix(path, adjustedPrefix)
        path = filepath.Join(isoOp.projectDir, path)
    }

    if strings.HasPrefix(filepath.Base(path), "ld-linux-x86-64") {
        path = filepath.Join(isoOp.projectDir, "/lib64", filepath.Base(path))
    }

    return path
}

//addSharedObjects: Attempt to open path as an elf file, and recurisely walk through all of that files imported libraries
//    If a untracked imported libraries is found, add it to the sharedObjects map with the path as its value
func (isoOp *importSharedObjectsOperation) addSharedObjects(fPath string) error {
    elfFile, elfLibs, err := openElfAndGetLibraries(fPath)
    if err != nil {
        return err // Could not open Imported Libraries
    }
    defer elfFile.Close()

    isoOp.setValidClass(elfFile.FileHeader.Class)

    for i := range elfLibs {
        if _, ok := isoOp.sharedObjects[elfLibs[i]]; !ok {
            elfLibPath, found, err := isoOp.findLib(elfLibs[i], elfFile.FileHeader.Class)
            if err == nil && found {
                // Library path found, add to map and then search library for its own imported libraries
                isoOp.sharedObjects[elfLibs[i]] = elfLibPath
                if err := isoOp.addSharedObjects(elfLibPath); err != nil {
                    return err
                }
            } else if err == nil {
                // Library path not found, add to map
                isoOp.sharedObjects[elfLibs[i]] = elfLibPath
            } else {
                return err
            }
        }
    }

    return nil
}

func openElfAndGetLibraries(fPath string) (*elf.File, []string, error) {
    elfFile, err := elf.Open(fPath)
    if err != nil {
        return nil, nil, err // File is not a valid elf
    }
    elfLibs, err := elfFile.ImportedLibraries()
    if err != nil {
        elfFile.Close()
        return nil, nil, err // Could not open Imported Libraries
    }

    return elfFile, elfLibs, nil
}

// Find the path of a library given the name and its elf class
func (isoOp *importSharedObjectsOperation) findLib(libName string, class elf.Class) (string, bool, error) {
    for _, ldPath := range isoOp.ldPATHS {
        potentialPath := filepath.Join(ldPath, libName)

        // Check if potentialPath can be stat'd
        if _, err := os.Stat(potentialPath); err != nil && !os.IsNotExist(err) {
            return "", false, fmt.Errorf("unable to stat candidate dependency: %w", err)
        } else if err != nil {
            continue
        }

        l, err := elf.Open(potentialPath)
        if err != nil {
            return "", false, errorDependencyScan(err)
        }

        if l.FileHeader.Class == class {
            isoOp.logger.Debugf("Found Path for library: %s at %s", libName, potentialPath)
            return potentialPath, true, nil
        }
    }

    isoOp.logger.Debugf("Could not find Path for library: %s", libName)
    // Unable to find lib
    return "", false, nil
}

func (isoOp *importSharedObjectsOperation) setValidClass(libClass elf.Class) {
    if !isoOp.imported32bit && libClass == elf.ELFCLASS32 {
        isoOp.imported32bit = true
    }

    if !isoOp.imported64bit && libClass == elf.ELFCLASS64 {
        isoOp.imported64bit = true
    }
}

// getListOfElfPath: will scan projectDir and return a list of paths that consists of every file in the project directory
func (isoOp *importSharedObjectsOperation) getProjectFiles() ([]string, error) {
    var projectPaths = make([]string, 0)
    err := filepath.Walk(isoOp.projectDir,
        func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            if !info.IsDir() {
                projectPaths = append(projectPaths, path)
            }
            return nil
        })

    if err != nil {
        return nil, err
    }

    return projectPaths, nil
}

// ERRORS
func errorDependencyScan(err error) error {
    return fmt.Errorf("unable to scan candidate dependency: %w", err)
}