/* $Progeny$ */

/* pci.c - Scan the PCI bus
 *
 * AUTHOR: John R. Daily <jdaily@progeny.com>
 *
 * Copyright 2002 Hewlett-Packard Company
 * Copyright 2001, 2002 Progeny Linux Systems, Inc.
 * Copyright (C) 1998-2000 MandrakeSoft
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>

#include <discover/sysdep.h>
#include <discover/utils.h>

/*
 * We just need a path length sufficiently long for paths like
 * /proc/bus/pci/02/07.0
 */
#ifdef _MAX_PATH_LEN
#undef _MAX_PATH_LEN
#endif
#define _MAX_PATH_LEN 256

/*
 * This function is specific to each sysdep.
 */
static void
_discover_sysdep_init(discover_sysdep_data_t *node)
{
    node->busclass = _discover_xmalloc(5);
    node->vendor = _discover_xmalloc(5);
    node->model = _discover_xmalloc(5);
}

discover_sysdep_data_t *
_discover_get_pci_raw_proc(void)
{
    FILE *devices_file;
    int individual_device_file;
    char *line = NULL;
    size_t len = 0;
    discover_sysdep_data_t *head = NULL, *node, *last = NULL;
    unsigned int i;
    unsigned int bus;
    unsigned int slot;
    unsigned int function;
    unsigned int sfcombo;  /* Slot/function combination byte */
    char file_path[_MAX_PATH_LEN];
    u_int16_t devtype;

    if ((devices_file = fopen(PATH_PROC_PCI, "r"))) {
        while (getline(&line, &len, devices_file) >= 0) {
            if (line[0] == '\n' || line[0] == '#') {
                continue;
            }

            node = _discover_sysdep_data_new();
            _discover_sysdep_init(node);

            if (sscanf(line, "%02x%02x\t%4s%4s", &bus, &sfcombo,
                       node->vendor, node->model) != 4) {
                perror("Failed to read expected data from devices file");
                exit(-1);
            }

            /*
             * The first 5 bits of the 2nd bite refer to the slot in the
             * PCI bus where this device can be found.  The last 3 bits
             * describe the function this card is performing in this
             * context.
             */
            slot = (sfcombo & 0xf8) >> 3;
            function = (sfcombo & 0x7);


            /*
             * Now we can read the busclass from the individual PCI file
             * in /proc/bus/pci/<bus>/<function>.<slot> and extract the
             * device type.
             */
            if (snprintf(file_path, _MAX_PATH_LEN, "%s/%02x/%02x.%1x",
                        PATH_PROC_PCI_DIR,
                        bus, slot, function) >= _MAX_PATH_LEN) {
                fprintf(stderr, "Path to PCI device file too long.\n");
                exit(-1);
            }

            /* We use open instead of fopen because fopen will give us
             * a buffered file which causes a read-ahead.  On some
             * devices reading undefined parts of the PCI
             * configuration causes the system to crash.
             */
            individual_device_file = open(file_path, O_RDONLY);

            if (individual_device_file == -1) {
                perror("Unable to locate device file");
                exit(-1);
            }

            lseek(individual_device_file, 5 * sizeof(u_int16_t), SEEK_SET);
            if (read(individual_device_file, &devtype,
                     sizeof(u_int16_t)) != sizeof(u_int16_t)) {
                perror("Unable to read device type from file");
                exit(-1);
            }

            close(individual_device_file);

#ifdef WORDS_BIGENDIAN
            /* If we're on a big-endian machine, then convert DEVTYPE
               to little-endian so we don't have mismatches. */
            devtype = ((devtype << 8) & 0xff00) | ((devtype >> 8) & 0xff);
#endif /* WORD_BIGENDIAN */

            snprintf(node->busclass, 5, "%.4x", devtype);

            if (head == NULL) {
                head = node;
                last = head;
            } else {
                last->next = node;
                last = node;
            }
        }
        free(line);
        fclose(devices_file);
    }
    return head;
}

discover_sysdep_data_t *
_discover_get_pci_raw_sys(void)
{
    discover_sysdep_data_t *head = NULL, *node, *last = NULL;
    FILE *f;
    DIR *pciDir;
    struct dirent *pci_device_entry;
    unsigned int len;
    char *device_dir, *line, *class, *vendor, *model, *p;
    char **device_dir_list = NULL;
    size_t device_dir_list_len, device_dir_index, device_dir_index2;
    char path[256];

    /* Open the directory containing all the PCI device dirs. */
    pciDir = opendir(PATH_SYS_PCI);
    if (pciDir == NULL)
        return _discover_get_pci_raw_proc();

    /* 
     * The order of links in PATH_SYS_PCI is not sorted.  Since 
     * module load order can affect things like device naming, 
     * we should collect the device directory names and sort them.  
     * We need to sort so that we mimic the order provided in 
     * PATH_PROC_PCI, so upgrades from 2.4 to 2.6 aren't affected.
     * (Perniciously, the unsorted order in PATH_SYS_PCI seems to be 
     * the exact opposite of that in PATH_PROC_PCI, thus guaranteeing 
     * that modules will be loaded in the opposite order without a sort.)
     */

    for (pci_device_entry = readdir(pciDir); pci_device_entry;
         pci_device_entry = readdir(pciDir)) {
        device_dir = strdup(pci_device_entry->d_name);
        if (device_dir == NULL)
            continue;
        if (device_dir[0] == '.') {
            free(device_dir);
            continue;
        }

        if (device_dir_list == NULL) {
            device_dir_list = (char **)malloc(sizeof(char *));
            device_dir_list_len = 1;
        } else {
            device_dir_list = 
                (char **)realloc(device_dir_list,
                                 sizeof(char *) * ++device_dir_list_len);
        }

        if (device_dir_list != NULL) {
            device_dir_list[device_dir_list_len - 1] = device_dir;
        } else {
            free(device_dir);
        }
    }

    closedir(pciDir);

    if (device_dir_list == NULL)
        return _discover_get_pci_raw_proc();

    /* Do the sort. */
    /* XXX: this turns out to not quite mimic the order we need. */
    /* qsort(device_dir_list, device_dir_list_len, sizeof(char *), strcmp); */

    /* For now, observe the pernicious nature of /sys and reverse the
       raw unsorted order. */
    device_dir_index = 0;
    device_dir_index2 = device_dir_list_len - 1;
    while (device_dir_index < device_dir_index2) {
        device_dir = device_dir_list[device_dir_index];
        device_dir_list[device_dir_index] = device_dir_list[device_dir_index2];
        device_dir_list[device_dir_index2] = device_dir;

        device_dir_index++;
        device_dir_index2--;
    }

    /* Loop through the PCI device dirs. */
    class = vendor = model = NULL;
    for (device_dir_index = 0; device_dir_index < device_dir_list_len;
         device_dir_index++) {
        device_dir = device_dir_list[device_dir_index];
        if (device_dir == NULL)
            continue;

        /* Clean up from tne last loop, if necessary. */
        if (class) {
            free(class);
            class = NULL;
        }
        if (vendor) {
            free(vendor);
            vendor = NULL;
        }
        if (model) {
            free(model);
            model = NULL;
        }

        /* Read each of the three files holding the busclass,
           vendor, and model information. */
        snprintf(path, 256, "%s/%s/class", PATH_SYS_PCI, device_dir);
        f = fopen(path, "r");
        if (!f) continue;
        getline(&class, &len, f);
        fclose(f);
        class[6] = '\0';
        for (p = class; *(p + 2) != '\0'; p++) *p = *(p + 2);
        *p = '\0';

        snprintf(path, 256, "%s/%s/vendor", PATH_SYS_PCI, device_dir);
        f = fopen(path, "r");
        if (!f) continue;
        getline(&vendor, &len, f);
        fclose(f);
        vendor[6] = '\0';
        for (p = vendor; *(p + 2) != '\0'; p++) *p = *(p + 2);
        *p = '\0';

        snprintf(path, 256, "%s/%s/device", PATH_SYS_PCI, device_dir);
        f = fopen(path, "r");
        if (!f) continue;
        getline(&model, &len, f);
        fclose(f);
        model[6] = '\0';
        for (p = model; *(p + 2) != '\0'; p++) *p = *(p + 2);
        *p = '\0';

        /* Create a new sysdep node and populate it. */
        node = _discover_sysdep_data_new();
        if (!node) continue;

        node->busclass = class;
        node->vendor = vendor;
        node->model = model;

        class = vendor = model = NULL;

        /* Stick the new node in the list. */
        if (head == NULL) {
            head = node;
            last = head;
        } else {
            last->next = node;
            last = node;
        }

        /* Clean up the directory list memory as we go. */
        free(device_dir);
    }

    free(device_dir_list);
    return head;
}

discover_sysdep_data_t *
_discover_get_pci_raw(void)
{
    if (!access(PATH_SYS_PCI, R_OK))
        return _discover_get_pci_raw_sys();
    else
        return _discover_get_pci_raw_proc();
}


/*
 * Local variables:
 * c-file-style: "progeny"
 * indent-tabs-mode: nil
 * End:
 */
/* vim: set cin fo=tcroq sw=4 et sts=4 tw=75: */
