/* * Mibble MIB Parser (www.mibble.org) * * See LICENSE.txt for licensing information. * * Copyright (c) 2004-2017 Per Cederberg. All rights reserved. */ package net.percederberg.mibble; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.Reader; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import net.percederberg.grammatica.parser.ParserCreationException; import net.percederberg.grammatica.parser.ParserLogException; import net.percederberg.mibble.asn1.Asn1Parser; import net.percederberg.mibble.value.ObjectIdentifierValue; /** * A MIB loader. This class contains a search path for locating MIB * files, and also holds a reference to previously loaded MIB files * to avoid loading the same file multiple times. The MIB search path * consists of directories with MIB files that can be imported into * other MIBs. The search path directories can either be normal file * system directories or resource directories. By default the search * path contains resource directories containing standard IANA and * IETF MIB files (packaged in the Mibble JAR file).<p> * * The MIB loader searches for MIB files in a specific order. First, * the file system directories in the search path are scanned for * files with the same name as the MIB module being imported. This * search ignores any file name extensions and compares the base file * name in case-insensitive mode. If this search fails, the resource * directories are searched for a file having the exact name of the * MIB module being imported (case sensitive). The last step, if both * the previous ones have failed, is to open the files in the search * path one by one to check the MIB module names specified inside. * Note that this may be slow for large directories with many files, * and it is therefore recommended to always name the MIB files * according to their module name.<p> * * The MIB loader is not thread-safe, i.e. it cannot be used * concurrently in multiple threads. * * @author Per Cederberg * @version 2.10 * @since 2.0 */ public class MibLoader { /** * The MIB file directory caches. This is also a list of the MIB * file search path, as each directory on the path has its own * cache. If a MIB isn't found among these directories, the * resource directories will be attempted. */ private ArrayList<MibLocator> dirCaches = new ArrayList<>(); /** * The MIB file resource directories. This is a list of Java class * loader resource directories to search for MIB files. These * directories can be used to store MIB files as resources inside * a JAR file. */ private ArrayList<String> resources = new ArrayList<>(); /** * The MIB files loaded. This maps the MIB names to the loaded * MIB objects (loaded with this loaded). This is used to avoid * loading duplicate MIB files. */ private LinkedHashMap<String,Mib> mibs = new LinkedHashMap<>(); /** * The queue of MIB files to load. This queue contains either * MIB module names or MibSource objects. */ private ArrayList<Object> queue = new ArrayList<>(); /** * The default MIB context. */ private DefaultContext context = new DefaultContext(); /** * The ASN.1 parser used (and reused) for all MIB files. */ private Asn1Parser parser = null; /** * Creates a new MIB loader. */ public MibLoader() { addResourceDir("mibs/iana"); addResourceDir("mibs/ietf"); } /** * Checks if a directory is in the MIB search path. If a file is * specified instead of a directory, this method checks if the * parent directory is in the MIB search path. * * @param dir the directory or file to check * * @return true if the directory is in the MIB search path, or * false otherwise * * @since 2.9 */ public boolean hasDir(File dir) { if (dir == null) { dir = new File("."); } else if (!dir.isDirectory()) { dir = dir.getParentFile(); } for (MibLocator cache : dirCaches) { if (cache.getDir().equals(dir)) { return true; } } return false; } /** * Returns all the directories in the MIB search path. If a tree * of directories has been added, all the individual directories * will be returned by this method. * * @return the directories in the MIB search path * * @since 2.9 */ public File[] getDirs() { File[] res = new File[dirCaches.size()]; for (int i = 0; i < dirCaches.size(); i++) { MibLocator cache = dirCaches.get(i); res[i] = cache.getDir(); } return res; } /** * Adds a directory to the MIB search path. If the directory * specified is null, the current working directory will be added. * * @param dir the directory to add */ public void addDir(File dir) { if (dir == null) { dir = new File("."); } if (!hasDir(dir) && dir.isDirectory()) { dirCaches.add(new MibLocator(dir)); } } /** * Adds directories to the MIB search path. * * @param dirs the directories to add */ public void addDirs(File[] dirs) { for (File file : dirs) { addDir(file); } } /** * Adds a directory and all subdirectories to the MIB search path. * If the directory specified is null, the current working * directory (and subdirectories) will be added. * * @param dir the directory to add */ public void addAllDirs(File dir) { if (dir == null) { dir = new File("."); } addDir(dir); for (File file : dir.listFiles()) { if (file.isDirectory()) { addAllDirs(file); } } } /** * Removes a directory from the MIB search path. * * @param dir the directory to remove */ public void removeDir(File dir) { for (int i = 0; i < dirCaches.size(); i++) { MibLocator cache = dirCaches.get(i); if (cache.getDir().equals(dir)) { dirCaches.remove(i--); } } } /** * Removes all directories from the MIB search path. */ public void removeAllDirs() { dirCaches.clear(); } /** * Checks if a directory is in the MIB resource path. The * resource search path is used for searching for MIB files with * the ClassLoader (i.e. MIB files on the Java classpath) and is * a secondary alternative to the directory search path. Note * that the MIB files stored as resources must have the EXACT MIB * name, i.e. no file extensions can be used and name casing is * important. * * @param dir the directory to check * * @return true if the directory is in the MIB resource path, or * false otherwise * * @since 2.9 */ public boolean hasResourceDir(String dir) { return resources.contains(dir); } /** * Returns all the directories in the MIB resource path. The * resource search path is used for searching for MIB files with * the ClassLoader (i.e. MIB files on the Java classpath) and is * a secondary alternative to the directory search path. Note * that the MIB files stored as resources must have the EXACT MIB * name, i.e. no file extensions can be used and name casing is * important. * * @return the directories in the MIB resource path * * @since 2.9 */ public String[] getResourceDirs() { return resources.toArray(new String[resources.size()]); } /** * Adds a directory to the MIB resource search path. The resource * search path is used for searching for MIB files with the * ClassLoader (i.e. MIB files on the Java classpath) and is a * secondary alternative to the directory search path. Note that * the MIB files stored as resources must have the EXACT MIB * name, i.e. no file extensions can be used and name casing is * important. * * @param dir the resource directory to add * * @since 2.3 */ public void addResourceDir(String dir) { if (!hasResourceDir(dir)) { resources.add(dir); } } /** * Removes a directory from the MIB resource search path. The * resource search path can be used to load MIB files as resources * via the ClassLoader. * * @param dir the resource directory to remove * * @since 2.3 */ public void removeResourceDir(String dir) { resources.remove(dir); } /** * Removes all directories from the MIB resource search path. This * will also remove the default directories where the IANA and * IETF MIB are present, and may thus make this MIB loader mostly * unusable. Use this method with caution. * * @since 2.3 */ public void removeAllResourceDirs() { resources.clear(); } /** * Resets this loader. This means that all references to previuos * MIB files will be removed, forcing a reload of any imported * MIB.<p> * * Note that this is not the same operation as unloadAll, since * the MIB files previously loaded will be unaffected by this * this method (i.e. they remain possible to use). If the purpose * is to free all memory used by the loaded MIB files, use the * unloadAll() method instead. * * @see #unloadAll() */ public void reset() { mibs.clear(); queue.clear(); context = new DefaultContext(); } /** * Returns the default MIB context. This context contains the * symbols that are predefined for all MIB:s (such as 'iso'). * * @return the default MIB context */ public MibContext getDefaultContext() { return context; } /** * Searches the OID tree from the loaded MIB files for the best * matching value. The returned OID value will be the longest * matching OID value, but doesn't have to be an exact match. The * search requires the full numeric OID value (from the root). * * @param oid the numeric OID string to search for * * @return the best matching OID value, or * null if no partial match was found * * @see ObjectIdentifierValue#find(String) * @since 2.10 */ public ObjectIdentifierValue getOid(String oid) { return context.findOid(oid); } /** * Returns the "iso" root object identifier value (OID). This OID * is the root for SNMP objects. Note that "ccitt" and * "joint-iso-ccitt" are also roots of the OID tree, but not * returned by this method (use a search in the default context * to find them). * * @return the root object identifier value ("iso" OID) * * @see #getOid(String) * @see #getDefaultContext() * @since 2.7 */ public ObjectIdentifierValue getRootOid() { MibSymbol symbol = context.findSymbol(DefaultContext.ISO, false); MibValue value = ((MibValueSymbol) symbol).getValue(); return (ObjectIdentifierValue) value; } /** * Returns a previously loaded MIB file. If the MIB file hasn't * been loaded, null will be returned. The MIB is identified by * it's MIB name (i.e. the module name). * * @param name the MIB (module) name * * @return the MIB module if found, or * null otherwise */ public Mib getMib(String name) { return mibs.get(name); } /** * Returns a previously loaded MIB file. If the MIB file hasn't * been loaded, null will be returned. The MIB is identified by * it's file name.<p> * * Note that if the file contained several MIB modules, this * method will only return the first one. Use getMibs(File) to * retrieve all. * * @param file the MIB file * * @return the first MIB module if found, or * null otherwise * * @since 2.3 */ public Mib getMib(File file) { for (Mib mib : mibs.values()) { if (mib.equals(file)) { return mib; } } return null; } /** * Returns a map of all MIB names and MIB files. If no MIB files * have been loaded, an empty map will be returned. The map is * ordered by load order. * * @return a map of MIB names to MIB objects * * @since 2.10 */ public Map<String,Mib> getMibs() { return mibs; } /** * Returns a map of all MIBs from a file. Normally, this is only * a single MIB, but some files may contain multiple MIBs. If the * file hasn't been loaded, an empty map will be returned. The * map is ordered by load order. * * @param file the MIB file * * @return a map of MIB names to MIB objects * * @since 2.10 */ public Map<String,Mib> getMibs(File file) { LinkedHashMap<String,Mib> res = new LinkedHashMap<>(); for (Mib mib : mibs.values()) { if (mib.equals(file)) { res.put(mib.getName(), mib); } } return res; } /** * Returns a map of all MIBs explicitly (or implicitly) loaded. * If no MIB files have been loaded, an empty map will be * returned. The map is ordered by load order. * * @param loaded the explicitly loaded MIB flag * * @return a map of MIB names to MIB objects * * @since 2.10 * * @see Mib#isLoaded() */ public Map<String,Mib> getMibs(boolean loaded) { LinkedHashMap<String,Mib> res = new LinkedHashMap<>(); for (Mib mib : mibs.values()) { if (mib.isLoaded() == loaded) { res.put(mib.getName(), mib); } } return res; } /** * Returns all previously loaded MIB files. If no MIB files have * been loaded an empty array will be returned. * * @return an array with all loaded MIB files * * @since 2.2 */ public Mib[] getAllMibs() { return mibs.values().toArray(new Mib[mibs.size()]); } /** * Loads a MIB file with the specified base name. The file is * searched for in the MIB search path. The MIB is identified by * it's MIB name (i.e. the module name). This method will also * load all imported MIB:s if not previously loaded by this * loader. If a MIB with the same name has already been loaded, it * will be returned directly instead of reloading it. * * @param name the MIB name (filename without extension) * * @return the MIB module loaded * * @throws IOException if the MIB file couldn't be found in the * MIB search path * @throws MibLoaderException if the MIB file couldn't be loaded * correctly */ public Mib load(String name) throws IOException, MibLoaderException { Mib mib = getMib(name); if (mib == null) { MibSource src = locate(name); if (src == null) { throw new FileNotFoundException("couldn't locate MIB: '" + name + "'"); } mib = load(src); } else { mib.setLoaded(true); } return mib; } /** * Loads a MIB file. This method will also load all imported MIB:s * if not previously loaded by this loader. If a MIB with the same * file name has already been loaded, it will be returned directly * instead of reloading it.<p> * * Note that if a file contains several MIB modules, this method * will only return the first one (although all are loaded). Use * getMibs(File) to retrieve all. * * @param file the MIB file * * @return the first MIB module loaded * * @throws IOException if the MIB file couldn't be read * @throws MibLoaderException if the MIB file couldn't be loaded * correctly */ public Mib load(File file) throws IOException, MibLoaderException { Map<String, Mib> found = getMibs(file); for (Mib mib : found.values()) { mib.setLoaded(true); } if (found.size() <= 0) { return load(new MibSource(file)); } else { return found.values().iterator().next(); } } /** * Loads a MIB file from the specified URL. This method will also * load all imported MIB:s if not previously loaded by this * loader.<p> * * Note that if the URL data contains several MIB modules, this * method will only return the first one (although all are * loaded). * * @param url the URL containing the MIB * * @return the first MIB module loaded * * @throws IOException if the MIB URL couldn't be read * @throws MibLoaderException if the MIB file couldn't be loaded * correctly * * @since 2.3 */ public Mib load(URL url) throws IOException, MibLoaderException { return load(new MibSource(url)); } /** * Loads a MIB file from the specified input reader. This method * will also load all imported MIB:s if not previously loaded by * this loader.<p> * * Note that if the input data contains several MIB modules, this * method will only return the first one (although all are * loaded). * * @param input the input stream containing the MIB * * @return the first MIB module loaded * * @throws IOException if the input stream couldn't be read * @throws MibLoaderException if the MIB file couldn't be loaded * correctly * * @since 2.3 */ public Mib load(Reader input) throws IOException, MibLoaderException { return load(new MibSource(input)); } /** * Loads a MIB. This method will also load all imported MIB:s if * not previously loaded by this loader.<p> * * Note that if the source contains several MIB modules, this * method will only return the first one (although all are * loaded). * * @param src the MIB source * * @return the first MIB module loaded * * @throws IOException if the MIB couldn't be found * @throws MibLoaderException if one of the MIB:s couldn't be * loaded correctly * * @since 2.10 */ public Mib load(MibSource src) throws IOException, MibLoaderException { queue.clear(); queue.add(src); return loadQueue(); } /** * Unloads a MIB. This method will remove the loader reference to * a previously loaded MIB if no other MIBs are depending on it. * This method attempts to free the memory used by the MIB, as it * clears both the loader and internal MIB references to the data * structures (thereby allowing the garbage collector to recover * the memory used if no other references exist). Other MIB:s * should be unaffected by this operation. * * @param name the MIB name * * @throws MibLoaderException if the MIB couldn't be unloaded * due to dependencies from other loaded MIBs * * @see #reset * * @since 2.3 */ public void unload(String name) throws MibLoaderException { unload(getMib(name)); } /** * Unloads a MIB. This method will remove the loader reference to * a previously loaded MIB if no other MIBs are depending on it. * This method attempts to free the memory used by the MIB, as it * clears both the loader and internal MIB references to the data * structures (thereby allowing the garbage collector to recover * the memory used if no other references exist). Other MIB:s * should be unaffected by this operation. * * @param file the MIB file * * @throws MibLoaderException if the MIB couldn't be unloaded * due to dependencies from other loaded MIBs * * @see #reset * * @since 2.3 */ public void unload(File file) throws MibLoaderException { Iterator<Mib> iter = mibs.values().iterator(); while (iter.hasNext()) { Mib mib = iter.next(); if (mib.equals(file)) { unload(mib); return; } } } /** * Unloads a MIB. This method will remove the loader reference to * a previously loaded MIB if no other MIBs are depending on it. * This method attempts to free the memory used by the MIB, as it * clears both the loader and internal MIB references to the data * structures (thereby allowing the garbage collector to recover * the memory used if no other references exist). Other MIB:s * should be unaffected by this operation. * * @param mib the MIB * * @throws MibLoaderException if the MIB couldn't be unloaded * due to dependencies from other loaded MIBs * * @see #reset * * @since 2.3 */ public void unload(Mib mib) throws MibLoaderException { if (mib != null) { Mib[] referers = mib.getImportingMibs(); if (referers.length > 0) { String msg = "cannot be unloaded due to reference in " + referers[0]; throw new MibLoaderException(msg); } mibs.remove(mib.getName()); mib.clear(); } } /** * Unloads all MIBs loaded by this loaded (since the last reset). * This method attempts to free all the memory used by the MIBs, * as it clears both the loader and internal MIB references to * the data structures (thereby allowing the garbage collector to * recover the memory used if no other references exist). Note * that no previous MIBs returned by this loader should be * accessed after this method has been called.<p> * * In order to just reset the MIB loader to force re-loading of * MIB files, use the reset() method instead which will leave the * MIBs unaffected. * * @see #reset() * @since 2.9 */ public void unloadAll() { Iterator<Mib> iter = mibs.values().iterator(); while (iter.hasNext()) { iter.next().clear(); } reset(); } /** * Schedules the loading of a MIB file. The file is added to the * queue of MIB files to be loaded, unless it is already loaded * or in the queue. The MIB file search is postponed until the * MIB is to be loaded, avoiding loading if the MIB name was * defined in another MIB file in the queue. * * @param name the MIB name (filename without extension) */ void scheduleLoad(String name) { if (getMib(name) == null && !queue.contains(name)) { queue.add(name); } } /** * Loads all MIB files in the loader queue. New entries may be * added to the queue while loading a MIB, as a result of * importing other MIB files. This method will either load all * MIB files in the queue or none (if errors were encountered). * * @return the first MIB module loaded * * @throws IOException if the MIB couldn't be found * @throws MibLoaderException if one of the MIB:s couldn't be * loaded correctly */ private Mib loadQueue() throws IOException, MibLoaderException { // Parse MIB files in queue MibLoaderLog log = new MibLoaderLog(); ArrayList<Mib> processed = new ArrayList<>(); Mib firstMib = null; while (queue.size() > 0) { try { boolean loaded = false; MibSource src = null; Object obj = queue.get(0); if (obj instanceof MibSource) { loaded = true; src = (MibSource) obj; } else if (getMib((String) obj) == null) { src = locate((String) obj); } if (src != null && getMib(src.getFile()) == null) { List<Mib> list = parseMib(src, log); for (Mib mib : list) { mib.setLoaded(loaded); mibs.put(mib.getName(), mib); if (firstMib == null) { firstMib = mib; } } processed.addAll(list); } } catch (MibLoaderException e) { // Do nothing, errors are already in the log } queue.remove(0); } // Initialize all parsed MIB files in reverse order for (int i = processed.size() - 1; i >= 0; i--) { try { processed.get(i).initialize(); } catch (MibLoaderException e) { // Do nothing, errors are already in the log } } // Validate all parsed MIB files in reverse order for (int i = processed.size() - 1; i >= 0; i--) { try { processed.get(i).validate(); } catch (MibLoaderException e) { // Do nothing, errors are already in the log } } // Handle errors if (log.errorCount() > 0) { for (Mib mib : processed) { mibs.remove(mib.getName()); mib.clear(); } throw new MibLoaderException(log); } return firstMib; } /** * Parses a MIB input source and returns the MIB modules found. * This method may read the MIB either from file, URL or input * stream. * * @param src the MIB source to parse * @param log the MIB log to use for errors * * @return the list of MIB modules created * * @throws IOException if the MIB couldn't be found * @throws MibLoaderException if the MIB couldn't be parsed * or analyzed correctly */ private ArrayList<Mib> parseMib(MibSource src, MibLoaderLog log) throws IOException, MibLoaderException { MibAnalyzer analyzer = new MibAnalyzer(src.getFile(), this, log); try ( Reader input = src.getReader(); ) { if (parser == null) { parser = new Asn1Parser(input, analyzer); parser.getTokenizer().setUseTokenList(true); } else { parser.reset(input, analyzer); } parser.parse(); return analyzer.getMibs(); } catch (ParserCreationException e) { String msg = "parser creation error in ASN.1 parser: " + e.getMessage(); log.addInternalError(msg); throw new MibLoaderException(log); } catch (ParserLogException e) { log.addAll(src.getFile(), e); throw new MibLoaderException(log); } finally { analyzer.reset(); } } /** * Searches for a MIB in the search path. The name specified * should be the MIB name. If a matching file name isn't found in * the directory search path, the contents in the base resource * path are also tested. Finally, if no MIB file has been found, * the files in the search path will be opened regardless of file * name to perform a small heuristic test for the MIB in question. * * @param name the MIB name * * @return the MIB found, or * null if no MIB was found */ private MibSource locate(String name) { for (MibLocator cache : dirCaches) { MibSource src = cache.findByName(name); if (src != null) { return src; } } ClassLoader loader = getClass().getClassLoader(); for (String path : resources) { URL url = loader.getResource(path + "/" + name); if (url != null) { return new MibSource(name, url); } } for (MibLocator cache : dirCaches) { MibSource src = cache.findByContent(name); if (src != null) { return src; } } return null; } }