URI Routing overhaul

 - Allow multiple levels of controller directories (supersedes PRs #390, #2439)
 - Add support for per-directory 'defaul_controller' and '404_override' (resolves issue #2611; supersedes PR #939)
 - Fixed a bug where default_controller was called instead of triggering 404 if the current route is inside a directory
 - Removed a few calls from CI_Router to CI_URI that made a necessity for otherwise internal CI_URI methods to be public:

    - Removed CI_URI::_fetch_uri_string() and moved its logic into CI_URI::__construct()
    - Removed CI_URI::_remove_url_suffix, CI_URI::_explode_segments() and moved their logic into CI_URI::_set_uri_string()
    - Removed CI_URI::_reindex_segments() altogether ( doesn't need further manipulation, while  is
      public anyway and can be properly (and more effectively) replaced on the spot)
diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php
index cc12f14..74a9eb0 100644
--- a/system/core/CodeIgniter.php
+++ b/system/core/CodeIgniter.php
@@ -53,10 +53,10 @@
  */
 	if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/constants.php'))
 	{
-		require(APPPATH.'config/'.ENVIRONMENT.'/constants.php');
+		require_once(APPPATH.'config/'.ENVIRONMENT.'/constants.php');
 	}
 
-	require(APPPATH.'config/constants.php');
+	require_once(APPPATH.'config/constants.php');
 
 /*
  * ------------------------------------------------------
@@ -209,7 +209,7 @@
  *
  */
 	// Load the base controller class
-	require BASEPATH.'core/Controller.php';
+	require_once BASEPATH.'core/Controller.php';
 
 	/**
 	 * Reference to the CI_Controller method.
@@ -225,96 +225,117 @@
 
 	if (file_exists(APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php'))
 	{
-		require APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php';
+		require_once APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php';
 	}
 
-	// Load the local application controller
-	// Note: The Router class automatically validates the controller path using the router->_validate_request().
-	// If this include fails it means that the default controller in the Routes.php file is not resolving to something valid.
-	$class = ucfirst($RTR->class);
-	if ( ! file_exists(APPPATH.'controllers/'.$RTR->directory.$class.'.php'))
-	{
-		show_error('Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.');
-	}
-
-	include(APPPATH.'controllers/'.$RTR->directory.$class.'.php');
-
 	// Set a mark point for benchmarking
 	$BM->mark('loading_time:_base_classes_end');
 
 /*
  * ------------------------------------------------------
- *  Security check
+ *  Sanity checks
  * ------------------------------------------------------
  *
- *  None of the methods in the app controller or the
- *  loader class can be called via the URI, nor can
+ *  The Router class has already validated the request,
+ *  leaving us with 3 options here:
+ *
+ *	1) an empty class name, if we reached the default
+ *	   controller, but it didn't exist;
+ *	2) a query string which doesn't go through a
+ *	   file_exists() check
+ *	3) a regular request for a non-existing page
+ *
+ *  We handle all of these as a 404 error.
+ *
+ *  Furthermore, none of the methods in the app controller
+ *  or the loader class can be called via the URI, nor can
  *  controller methods that begin with an underscore.
  */
-	$method	= $RTR->method;
 
-	if ( ! class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method))
+	$e404 = FALSE;
+	$class = ucfirst($RTR->class);
+	$method = $RTR->method;
+
+	if (empty($class) OR ! file_exists(APPPATH.'controllers/'.$RTR->directory.$class.'.php'))
 	{
-		if ( ! empty($RTR->routes['404_override']))
-		{
-			if (sscanf($RTR->routes['404_override'], '%[^/]/%s', $class, $method) !== 2)
-			{
-				$method = 'index';
-			}
-
-			$class = ucfirst($class);
-
-			if ( ! class_exists($class, FALSE))
-			{
-				if ( ! file_exists(APPPATH.'controllers/'.$class.'.php'))
-				{
-					show_404($class.'/'.$method);
-				}
-
-				include_once(APPPATH.'controllers/'.$class.'.php');
-			}
-		}
-		else
-		{
-			show_404($class.'/'.$method);
-		}
-	}
-
-	if (method_exists($class, '_remap'))
-	{
-		$params = array($method, array_slice($URI->rsegments, 2));
-		$method = '_remap';
+		$e404 = TRUE;
 	}
 	else
 	{
+		require_once(APPPATH.'controllers/'.$RTR->directory.$class.'.php');
+
+		if ( ! class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method))
+		{
+			$e404 = TRUE;
+		}
+		elseif (method_exists($class, '_remap'))
+		{
+			$params = array($method, array_slice($URI->rsegments, 2));
+			$method = '_remap';
+		}
 		// WARNING: It appears that there are issues with is_callable() even in PHP 5.2!
 		// Furthermore, there are bug reports and feature/change requests related to it
 		// that make it unreliable to use in this context. Please, DO NOT change this
 		// work-around until a better alternative is available.
-		if ( ! in_array(strtolower($method), array_map('strtolower', get_class_methods($class)), TRUE))
+		elseif ( ! in_array(strtolower($method), array_map('strtolower', get_class_methods($class)), TRUE))
 		{
-			if (empty($RTR->routes['404_override']))
+			$e404 = TRUE;
+		}
+	}
+
+	if ($e404)
+	{
+		if ( ! empty($RTR->routes['404_override']))
+		{
+			if (sscanf($RTR->routes['404_override'], '%[^/]/%s', $error_class, $error_method) !== 2)
 			{
-				show_404($class.'/'.$method);
-			}
-			elseif (sscanf($RTR->routes['404_override'], '%[^/]/%s', $class, $method) !== 2)
-			{
-				$method = 'index';
+				$error_method = 'index';
 			}
 
-			$class = ucfirst($class);
+			$error_class = ucfirst($error_class);
 
-			if ( ! class_exists($class, FALSE))
+			if ( ! class_exists($error_class, FALSE))
 			{
-				if ( ! file_exists(APPPATH.'controllers/'.$class.'.php'))
+				if (file_exists(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php'))
 				{
-					show_404($class.'/'.$method);
+					require_once(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php');
+					$e404 = ! class_exists($error_class, FALSE);
 				}
-
-				include_once(APPPATH.'controllers/'.$class.'.php');
+				// Were we in a directory? If so, check for a global override
+				elseif ( ! empty($RTR->directory) && file_exists(APPPATH.'controllers/'.$error_class.'.php'))
+				{
+					require_once(APPPATH.'controllers/'.$error_class.'.php');
+					if (($e404 = ! class_exists($error_class, FALSE)) === FALSE)
+					{
+						$RTR->directory = '';
+					}
+				}
+			}
+			else
+			{
+				$e404 = FALSE;
 			}
 		}
 
+		// Did we reset the $e404 flag? If so, set the rsegments, starting from index 1
+		if ( ! $e404)
+		{
+			$class = $error_class;
+			$method = $error_method;
+
+			$URI->rsegments = array(
+				1 => $class,
+				2 => $method
+			);
+		}
+		else
+		{
+			show_404($RTR->directory.$class.'/'.$method);
+		}
+	}
+
+	if ($method !== '_remap')
+	{
 		$params = array_slice($URI->rsegments, 2);
 	}
 
diff --git a/system/core/Router.php b/system/core/Router.php
index 71530ff..e3c9115 100644
--- a/system/core/Router.php
+++ b/system/core/Router.php
@@ -91,6 +91,15 @@
 	 */
 	public $translate_uri_dashes = FALSE;
 
+	/**
+	 * Enable query strings flag
+	 *
+	 * Determines wether to use GET parameters or segment URIs
+	 *
+	 * @var	bool
+	 */
+	public $enable_query_strings = FALSE;
+
 	// --------------------------------------------------------------------
 
 	/**
@@ -106,6 +115,8 @@
 
 		$this->config =& load_class('Config', 'core');
 		$this->uri =& load_class('URI', 'core');
+
+		$this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE);
 		$this->_set_routing();
 
 		// Set any routing overrides that may exist in the main index file
@@ -146,26 +157,39 @@
 		// Are query strings enabled in the config file? Normally CI doesn't utilize query strings
 		// since URI segments are more search-engine friendly, but they can optionally be used.
 		// If this feature is enabled, we will gather the directory/class/method a little differently
-		$segments = array();
-		if ($this->config->item('enable_query_strings') === TRUE
-			&& ! empty($_GET[$this->config->item('controller_trigger')])
-			&& is_string($_GET[$this->config->item('controller_trigger')])
-		)
+		if ($this->enable_query_strings)
 		{
-			if (isset($_GET[$this->config->item('directory_trigger')]) && is_string($_GET[$this->config->item('directory_trigger')]))
+			$_d = $this->config->item('directory_trigger');
+			$_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';
+			if ($_d !== '')
 			{
-				$this->set_directory(trim($this->uri->filter_uri($_GET[$this->config->item('directory_trigger')])));
-				$segments[] = $this->directory;
+				$this->set_directory($this->uri->filter_uri($_d));
 			}
 
-			$this->set_class(trim($this->uri->filter_uri($_GET[$this->config->item('controller_trigger')])));
-			$segments[] = $this->class;
-
-			if ( ! empty($_GET[$this->config->item('function_trigger')]) && is_string($_GET[$this->config->item('function_trigger')]))
+			$_c = $this->config->item('controller_trigger');
+			if ( ! empty($_GET[$_c]))
 			{
-				$this->set_method(trim($this->uri->filter_uri($_GET[$this->config->item('function_trigger')])));
-				$segments[] = $this->method;
+				$this->set_class(trim($this->uri->filter_uri(trim($_GET[$_c]))));
+
+				$_f = $this->config->item('function_trigger');
+				if ( ! empty($_GET[$_f]))
+				{
+					$this->set_method(trim($this->uri->filter_uri($_GET[$_f])));
+				}
+
+				$this->uri->rsegments = array(
+					1 => $this->class,
+					2 => $this->method
+				);
 			}
+			else
+			{
+				$this->_set_default_controller();
+			}
+
+			// Routing rules don't apply to query strings and we don't need to detect
+			// directories, so we're done here
+			return;
 		}
 
 		// Load the routes.php file.
@@ -188,25 +212,58 @@
 			$this->routes = $route;
 		}
 
-		// Were there any query string segments? If so, we'll validate them and bail out since we're done.
-		if (count($segments) > 0)
+		// Is there anything to parse?
+		if ($this->uri->uri_string !== '')
 		{
-			return $this->_validate_request($segments);
+			$this->_parse_routes();
+		}
+		else
+		{
+			$this->_set_default_controller();
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set request route
+	 *
+	 * Takes an array of URI segments as input and sets the class/method
+	 * to be called.
+	 *
+	 * @used-by	CI_Router::_parse_routes()
+	 * @param	array	$segments	URI segments
+	 * @return	void
+	 */
+	protected function _set_request($segments = array())
+	{
+		$segments = $this->_validate_request($segments);
+		// If we don't have any segments left - try the default controller;
+		// WARNING: Directories get shifted out of the segments array!
+		if (empty($segments))
+		{
+			$this->_set_default_controller();
+			return;
 		}
 
-		// Fetch the complete URI string
-		$this->uri->_fetch_uri_string();
-
-		// Is there a URI string? If not, the default controller specified in the "routes" file will be shown.
-		if ($this->uri->uri_string == '')
+		if ($this->translate_uri_dashes === TRUE)
 		{
-			return $this->_set_default_controller();
+			$segments[0] = str_replace('-', '_', $segments[0]);
+			if (isset($segments[1]))
+			{
+				$segments[1] = str_replace('-', '_', $segments[1]);
+			}
 		}
 
-		$this->uri->_remove_url_suffix(); // Remove the URL suffix
-		$this->uri->_explode_segments(); // Compile the segments into an array
-		$this->_parse_routes(); // Parse any custom routing that may exist
-		$this->uri->_reindex_segments(); // Re-index the segment array so that it starts with 1 rather than 0
+		$this->set_class($segments[0]);
+		if (isset($segments[1]))
+		{
+			$this->set_method($segments[1]);
+		}
+
+		array_unshift($segments, NULL);
+		unset($segments[0]);
+		$this->uri->rsegments = $segments;
 	}
 
 	// --------------------------------------------------------------------
@@ -229,10 +286,20 @@
 			$method = 'index';
 		}
 
-		$this->_set_request(array($class, $method));
+		if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php'))
+		{
+			// This will trigger 404 later
+			return;
+		}
 
-		// re-index the routed segments array so it starts with 1 rather than 0
-		$this->uri->_reindex_segments();
+		$this->set_class($class);
+		$this->set_method($method);
+
+		// Assign routed segments, index starting from 1
+		$this->uri->rsegments = array(
+			1 => $class,
+			2 => $method
+		);
 
 		log_message('debug', 'No URI present. Default controller set.');
 	}
@@ -240,117 +307,35 @@
 	// --------------------------------------------------------------------
 
 	/**
-	 * Set request route
-	 *
-	 * Takes an array of URI segments as input and sets the class/method
-	 * to be called.
-	 *
-	 * @param	array	$segments	URI segments
-	 * @return	void
-	 */
-	protected function _set_request($segments = array())
-	{
-		$segments = $this->_validate_request($segments);
-
-		if (count($segments) === 0)
-		{
-			return $this->_set_default_controller();
-		}
-
-		if ($this->translate_uri_dashes === TRUE)
-		{
-			$segments[0] = str_replace('-', '_', $segments[0]);
-			if (isset($segments[1]))
-			{
-				$segments[1] = str_replace('-', '_', $segments[1]);
-			}
-		}
-
-		$this->set_class($segments[0]);
-		isset($segments[1]) OR $segments[1] = 'index';
-		$this->set_method($segments[1]);
-
-		// Update our "routed" segment array to contain the segments.
-		// Note: If there is no custom routing, this array will be
-		// identical to $this->uri->segments
-		$this->uri->rsegments = $segments;
-	}
-
-	// --------------------------------------------------------------------
-
-	/**
 	 * Validate request
 	 *
 	 * Attempts validate the URI request and determine the controller path.
 	 *
+	 * @used-by	CI_Router::_set_request()
 	 * @param	array	$segments	URI segments
-	 * @return	array	URI segments
+	 * @return	mixed	URI segments
 	 */
 	protected function _validate_request($segments)
 	{
-		if (count($segments) === 0)
+		$c = count($segments);
+		// Loop through our segments and return as soon as a controller
+		// is found or when such a directory doesn't exist
+		while ($c-- > 0)
 		{
-			return $segments;
-		}
+			$test = $this->directory
+				.ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
 
-		$test = ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
-
-		// Does the requested controller exist in the root folder?
-		if (file_exists(APPPATH.'controllers/'.$test.'.php'))
-		{
-			return $segments;
-		}
-
-		// Is the controller in a sub-folder?
-		if (is_dir(APPPATH.'controllers/'.$segments[0]))
-		{
-			// Set the directory and remove it from the segment array
-			$this->set_directory(array_shift($segments));
-			if (count($segments) > 0)
+			if ( ! file_exists(APPPATH.'controllers/'.$test.'.php') && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0]))
 			{
-				$test = ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
-
-				// Does the requested controller exist in the sub-directory?
-				if ( ! file_exists(APPPATH.'controllers/'.$this->directory.$test.'.php'))
-				{
-					if ( ! empty($this->routes['404_override']))
-					{
-						$this->directory = '';
-						return explode('/', $this->routes['404_override'], 2);
-					}
-					else
-					{
-						show_404($this->directory.$segments[0]);
-					}
-				}
-			}
-			else
-			{
-				// Is the method being specified in the route?
-				$segments = explode('/', $this->default_controller);
-				if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($segments[0]).'.php'))
-				{
-					$this->directory = '';
-				}
+				$this->set_directory(array_shift($segments), TRUE);
+				continue;
 			}
 
 			return $segments;
 		}
 
-		// If we've gotten this far it means that the URI does not correlate to a valid
-		// controller class. We will now see if there is an override
-		if ( ! empty($this->routes['404_override']))
-		{
-			if (sscanf($this->routes['404_override'], '%[^/]/%s', $class, $method) !== 2)
-			{
-				$method = 'index';
-			}
-
-			return array($class, $method);
-		}
-
-		// Nothing else to do at this point but show a 404
-		show_404($segments[0]);
+		// This means that all segments were actually directories
+		return $segments;
 	}
 
 	// --------------------------------------------------------------------
@@ -377,12 +362,14 @@
 			// Check default routes format
 			if (is_string($this->routes[$uri]))
 			{
-				return $this->_set_request(explode('/', $this->routes[$uri]));
+				$this->_set_request(explode('/', $this->routes[$uri]));
+				return;
 			}
 			// Is there a matching http verb?
 			elseif (is_array($this->routes[$uri]) && isset($this->routes[$uri][$http_verb]))
 			{
-				return $this->_set_request(explode('/', $this->routes[$uri][$http_verb]));
+				$this->_set_request(explode('/', $this->routes[$uri][$http_verb]));
+				return;
 			}
 		}
 
@@ -452,7 +439,8 @@
 					$val = preg_replace('#^'.$key.'$#', $val, $uri);
 				}
 
-				return $this->_set_request(explode('/', $val));
+				$this->_set_request(explode('/', $val));
+				return;
 			}
 		}
 
@@ -519,11 +507,19 @@
 	 * Set directory name
 	 *
 	 * @param	string	$dir	Directory name
+	 * @param	bool	$appent	Whether we're appending rather then setting the full value
 	 * @return	void
 	 */
-	public function set_directory($dir)
+	public function set_directory($dir, $append = FALSE)
 	{
-		$this->directory = str_replace(array('/', '.'), '', $dir).'/';
+		if ($append !== TRUE OR empty($this->directory))
+		{
+			$this->directory = str_replace('.', '', trim($dir, '/')).'/';
+		}
+		else
+		{
+			$this->directory .= str_replace('.', '', trim($dir, '/')).'/';
+		}
 	}
 
 	// --------------------------------------------------------------------
diff --git a/system/core/URI.php b/system/core/URI.php
index c83b7a7..80cf0b4 100644
--- a/system/core/URI.php
+++ b/system/core/URI.php
@@ -44,21 +44,21 @@
 	 *
 	 * @var	array
 	 */
-	public $keyval =	array();
+	public $keyval = array();
 
 	/**
 	 * Current URI string
 	 *
 	 * @var	string
 	 */
-	public $uri_string;
+	public $uri_string = '';
 
 	/**
 	 * List of URI segments
 	 *
 	 * @var	array
 	 */
-	public $segments =	array();
+	public $segments = array();
 
 	/**
 	 * Re-indexed list of URI segments
@@ -67,7 +67,7 @@
 	 *
 	 * @var	array
 	 */
-	public $rsegments =	array();
+	public $rsegments = array();
 
 	/**
 	 * Permitted URI chars
@@ -81,19 +81,50 @@
 	/**
 	 * Class constructor
 	 *
-	 * Simply globalizes the $RTR object. The front
-	 * loads the Router class early on so it's not available
-	 * normally as other classes are.
-	 *
 	 * @return	void
 	 */
 	public function __construct()
 	{
 		$this->config =& load_class('Config', 'core');
 
-		if ($this->config->item('enable_query_strings') !== TRUE OR is_cli())
+		// If query strings are enabled, we don't need to parse any segments.
+		// However, they don't make sense under CLI.
+		if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE)
 		{
 			$this->_permitted_uri_chars = $this->config->item('permitted_uri_chars');
+
+			// If it's a CLI request, ignore the configuration
+			if (is_cli() OR ($protocol = strtoupper($this->config->item('uri_protocol')) === 'CLI'))
+			{
+				$this->_set_uri_string($this->_parse_argv());
+			}
+			elseif ($protocol === 'AUTO')
+			{
+				// Is there a PATH_INFO variable? This should be the easiest solution.
+				if (isset($_SERVER['PATH_INFO']))
+				{
+					$this->_set_uri_string($_SERVER['PATH_INFO']);
+				}
+				// No PATH_INFO? Let's try REQUST_URI or QUERY_STRING then
+				elseif (($uri = $this->_parse_request_uri()) !== '' OR ($uri = $this->_parse_query_string()) !== '')
+				{
+					$this->_set_uri_string($uri);
+				}
+				// As a last ditch effor, let's try using the $_GET array
+				elseif (is_array($_GET) && count($_GET) === 1 && trim(key($_GET), '/') !== '')
+				{
+					$this->_set_uri_string(key($_GET));
+				}
+			}
+			elseif (method_exists($this, ($method = '_parse_'.strtolower($protocol))))
+			{
+				$this->_set_uri_string($this->$method());
+			}
+			else
+			{
+				$uri = isset($_SERVER[$protocol]) ? $_SERVER[$protocol] : @getenv($protocol);
+				$this->_set_uri_string($uri);
+			}
 		}
 
 		log_message('debug', 'URI Class Initialized');
@@ -102,75 +133,6 @@
 	// --------------------------------------------------------------------
 
 	/**
-	 * Fetch URI String
-	 *
-	 * @used-by	CI_Router
-	 * @return	void
-	 */
-	public function _fetch_uri_string()
-	{
-		$protocol = strtoupper($this->config->item('uri_protocol'));
-
-		if ($protocol === 'AUTO')
-		{
-			// Is the request coming from the command line?
-			if (is_cli())
-			{
-				$this->_set_uri_string($this->_parse_argv());
-				return;
-			}
-
-			// Is there a PATH_INFO variable? This should be the easiest solution.
-			if (isset($_SERVER['PATH_INFO']))
-			{
-				$this->_set_uri_string($_SERVER['PATH_INFO']);
-				return;
-			}
-
-			// Let's try REQUEST_URI then, this will work in most situations
-			if (($uri = $this->_parse_request_uri()) !== '')
-			{
-				$this->_set_uri_string($uri);
-				return;
-			}
-
-			// No REQUEST_URI either?... What about QUERY_STRING?
-			if (($uri = $this->_parse_query_string()) !== '')
-			{
-				$this->_set_uri_string($uri);
-				return;
-			}
-
-			// As a last ditch effort let's try using the $_GET array
-			if (is_array($_GET) && count($_GET) === 1 && trim(key($_GET), '/') !== '')
-			{
-				$this->_set_uri_string(key($_GET));
-				return;
-			}
-
-			// We've exhausted all our options...
-			$this->uri_string = '';
-			return;
-		}
-
-		if ($protocol === 'CLI')
-		{
-			$this->_set_uri_string($this->_parse_argv());
-			return;
-		}
-		elseif (method_exists($this, ($method = '_parse_'.strtolower($protocol))))
-		{
-			$this->_set_uri_string($this->$method());
-			return;
-		}
-
-		$uri = isset($_SERVER[$protocol]) ? $_SERVER[$protocol] : @getenv($protocol);
-		$this->_set_uri_string($uri);
-	}
-
-	// --------------------------------------------------------------------
-
-	/**
 	 * Set URI String
 	 *
 	 * @param 	string	$str
@@ -180,6 +142,32 @@
 	{
 		// Filter out control characters and trim slashes
 		$this->uri_string = trim(remove_invisible_characters($str, FALSE), '/');
+
+		if ($this->uri_string !== '')
+		{
+			// Remove the URL suffix, if present
+			if (($suffix = (string) $this->config->item('url_suffix')) !== '')
+			{
+				$slen = strlen($suffix);
+
+				if (substr($this->uri_string, -$slen) === $suffix)
+				{
+					$this->uri_string = substr($this->uri_string, 0, -$slen);
+				}
+			}
+
+			// Populate the segments array
+			foreach (explode('/', preg_replace('|/*(.+?)/*$|', '\\1', $this->uri_string)) as $val)
+			{
+				// Filter segments for security
+				$val = trim($this->filter_uri($val));
+
+				if ($val !== '')
+				{
+					$this->segments[] = $val;
+				}
+			}
+		}
 	}
 
 	// --------------------------------------------------------------------
@@ -240,36 +228,10 @@
 	// --------------------------------------------------------------------
 
 	/**
-	 * Remove relative directory (../) and multi slashes (///)
-	 *
-	 * Do some final cleaning of the URI and return it, currently only used in self::_parse_request_uri()
-	 *
-	 * @param	string	$url
-	 * @return	string
-	 */
-	protected function _remove_relative_directory($uri)
-	{
-		$uris = array();
-		$tok = strtok($uri, '/');
-		while ($tok !== FALSE)
-		{
-			if (( ! empty($tok) OR $tok === '0') && $tok !== '..')
-			{
-				$uris[] = $tok;
-			}
-			$tok = strtok('/');
-		}
-		return implode('/', $uris);
-	}
-
-	// --------------------------------------------------------------------
-
-	/**
 	 * Parse QUERY_STRING
 	 *
 	 * Will parse QUERY_STRING and automatically detect the URI from it.
 	 *
-	 * @used-by	CI_URI::_fetch_uri_string()
 	 * @return	string
 	 */
 	protected function _parse_query_string()
@@ -310,11 +272,36 @@
 	// --------------------------------------------------------------------
 
 	/**
+	 * Remove relative directory (../) and multi slashes (///)
+	 *
+	 * Do some final cleaning of the URI and return it, currently only used in self::_parse_request_uri()
+	 *
+	 * @param	string	$url
+	 * @return	string
+	 */
+	protected function _remove_relative_directory($uri)
+	{
+		$uris = array();
+		$tok = strtok($uri, '/');
+		while ($tok !== FALSE)
+		{
+			if (( ! empty($tok) OR $tok === '0') && $tok !== '..')
+			{
+				$uris[] = $tok;
+			}
+			$tok = strtok('/');
+		}
+
+		return implode('/', $uris);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
 	 * Filter URI
 	 *
 	 * Filters segments for malicious characters.
 	 *
-	 * @used-by	CI_Router
 	 * @param	string	$str
 	 * @return	string
 	 */
@@ -336,79 +323,6 @@
 	// --------------------------------------------------------------------
 
 	/**
-	 * Remove URL suffix
-	 *
-	 * Removes the suffix from the URL if needed.
-	 *
-	 * @used-by	CI_Router
-	 * @return	void
-	 */
-	public function _remove_url_suffix()
-	{
-		$suffix = (string) $this->config->item('url_suffix');
-
-		if ($suffix === '')
-		{
-			return;
-		}
-
-		$slen = strlen($suffix);
-
-		if (substr($this->uri_string, -$slen) === $suffix)
-		{
-			$this->uri_string = substr($this->uri_string, 0, -$slen);
-		}
-	}
-
-	// --------------------------------------------------------------------
-
-	/**
-	 * Explode URI segments
-	 *
-	 * The individual segments will be stored in the $this->segments array.
-	 *
-	 * @see		CI_URI::$segments
-	 * @used-by	CI_Router
-	 * @return	void
-	 */
-	public function _explode_segments()
-	{
-		foreach (explode('/', preg_replace('|/*(.+?)/*$|', '\\1', $this->uri_string)) as $val)
-		{
-			// Filter segments for security
-			$val = trim($this->filter_uri($val));
-
-			if ($val !== '')
-			{
-				$this->segments[] = $val;
-			}
-		}
-	}
-
-	// --------------------------------------------------------------------
-
-	/**
-	 * Re-index Segments
-	 *
-	 * Re-indexes the CI_URI::$segment array so that it starts at 1 rather
-	 * than 0. Doing so makes it simpler to use methods like
-	 * CI_URI::segment(n) since there is a 1:1 relationship between the
-	 * segment array and the actual segments.
-	 *
-	 * @used-by	CI_Router
-	 * @return	void
-	 */
-	public function _reindex_segments()
-	{
-		array_unshift($this->segments, NULL);
-		array_unshift($this->rsegments, NULL);
-		unset($this->segments[0]);
-		unset($this->rsegments[0]);
-	}
-
-	// --------------------------------------------------------------------
-
-	/**
 	 * Fetch URI Segment
 	 *
 	 * @see		CI_URI::$segments
@@ -714,7 +628,7 @@
 	{
 		global $RTR;
 
-		return ltrim($RTR->directory, '/').implode('/', $this->rsegment_array());
+		return ltrim($RTR->directory, '/').implode('/', $this->rsegments);
 	}
 
 }
diff --git a/tests/codeigniter/core/URI_test.php b/tests/codeigniter/core/URI_test.php
index 99d79bb..6589c1f 100644
--- a/tests/codeigniter/core/URI_test.php
+++ b/tests/codeigniter/core/URI_test.php
@@ -26,6 +26,10 @@
 
 	// --------------------------------------------------------------------
 
+	/*
+
+		This has been moved to the constructor
+
 	public function test_fetch_uri_string()
 	{
 		define('SELF', 'index.php');
@@ -86,9 +90,14 @@
 		// uri_protocol: REQUEST_URI
 		// uri_protocol: CLI
 	}
+	*/
 
 	// --------------------------------------------------------------------
 
+	/*
+
+		This has been moved into _set_uri_string()
+
 	public function test_explode_segments()
 	{
 		// Let's test the function's ability to clean up this mess
@@ -107,7 +116,7 @@
 			$this->assertEquals($a, $this->uri->segments);
 		}
 	}
-
+	*/
 	// --------------------------------------------------------------------
 
 	public function test_filter_uri()
@@ -145,23 +154,6 @@
 
 	// --------------------------------------------------------------------
 
-	public function test_remove_url_suffix()
-	{
-		$this->uri->config->set_item('url_suffix', '.html');
-
-		$this->uri->uri_string = 'controller/method/index.html';
-		$this->uri->_remove_url_suffix();
-
-		$this->assertEquals($this->uri->uri_string, 'controller/method/index');
-
-		$this->uri->uri_string = 'controller/method/index.htmlify.html';
-		$this->uri->_remove_url_suffix();
-
-		$this->assertEquals($this->uri->uri_string, 'controller/method/index.htmlify');
-	}
-
-	// --------------------------------------------------------------------
-
 	public function test_segment()
 	{
 		$this->uri->segments = array(1 => 'controller');
diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst
index 4712ed8..388bc28 100644
--- a/user_guide_src/source/changelog.rst
+++ b/user_guide_src/source/changelog.rst
@@ -389,6 +389,16 @@
 
 -  Core
 
+   -  :doc:`Routing <general/routing>` changes include:
+
+      -  Added support for multiple levels of controller directories.
+      -  Added support for per-directory *default_controller* and *404_override* classes.
+      -  Added possibility to route requests using HTTP verbs.
+      -  Added possibility to route requests using callbacks.
+      -  Added a new reserved route (*translate_uri_dashes*) to allow usage of dashes in the controller and method URI segments.
+      -  Deprecated methods ``fetch_directory()``, ``fetch_class()`` and ``fetch_method()`` in favor of their respective public properties.
+      -  Removed method ``_set_overrides()`` and moved its logic to the class constructor.
+
    -  :doc:`URI Library <libraries/uri>` changes include:
 
       -  Added conditional PCRE UTF-8 support to the "invalid URI characters" check and removed the ``preg_quote()`` call from it to allow more flexibility.
@@ -399,6 +409,9 @@
       -  Changed ``_parse_request_uri()`` to accept absolute URIs for compatibility with HTTP/1.1 as per `RFC2616 <http://www.ietf.org/rfc/rfc2616.txt>`.
       -  Added protected method ``_parse_query_string()`` to URI paths in the the **QUERY_STRING** value, like ``_parse_request_uri()`` does.
       -  Changed ``_fetch_uri_string()`` to try the **PATH_INFO** variable first when auto-detecting.
+      -  Removed methods ``_remove_url_suffix()``, ``_explode_segments()`` and moved their logic into ``_set_uri_string()``.
+      -  Removed method ``_fetch_uri_string()`` and moved its logic into the class constructor.
+      -  Removed method ``_reindex_segments()``.
 
    -  :doc:`Loader Library <libraries/loader>` changes include:
 
@@ -458,14 +471,6 @@
       -  Added ``$config['csrf_exclude_uris']``, which allows you list URIs which will not have the CSRF validation methods run.
       -  Modified method ``sanitize_filename()`` to read a public ``$filename_bad_chars`` property for getting the invalid characters list.
 
-   -  :doc:`URI Routing <general/routing>` changes include:
-
-      -  Added possibility to route requests using HTTP verbs.
-      -  Added possibility to route requests using callbacks.
-      -  Added a new reserved route (*translate_uri_dashes*) to allow usage of dashes in the controller and method URI segments.
-      -  Deprecated methods ``fetch_directory()``, ``fetch_class()`` and ``fetch_method()`` in favor of their respective public properties.
-      -  Removed method ``_set_overrides()`` and moved its logic to the class constructor.
-
    -  :doc:`Language Library <libraries/language>` changes include:
 
       -  Changed method ``load()`` to filter the language name with ``ctype_alpha()``.
@@ -671,6 +676,7 @@
 -  Fixed an edge case (#555) - incorrect browser version was reported for Opera 10+ due to a non-standard user-agent string.
 -  Fixed a bug (#133) - :doc:`Text Helper <helpers/text_helper>` :func:`ascii_to_entities()` stripped the last character if it happens to be in the extended ASCII group.
 -  Fixed a bug (#2822) - ``fwrite()`` was used incorrectly throughout the whole framework, allowing incomplete writes when writing to a network stream and possibly a few other edge cases.
+-  Fixed a bug where *default_controller* was called instad of triggering a 404 error if the current route is in a controller directory.
 
 Version 2.1.4
 =============