Skip to main content

subversion/
error.rs

1use subversion_sys::svn_error_t;
2
3/// Categorizes the kind of error that occurred based on SVN error code ranges.
4///
5/// This enum provides a way to programmatically distinguish between different
6/// error categories without parsing error messages. The categories correspond
7/// to SVN's internal error code organization.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum ErrorCategory {
10    /// Malformed input or argument errors (125000-129999)
11    BadInput,
12    /// XML parsing/generation errors (130000-134999)
13    Xml,
14    /// I/O errors (135000-139999)
15    Io,
16    /// Stream-related errors (140000-144999)
17    Stream,
18    /// Node (file/directory) errors (145000-149999)
19    Node,
20    /// Entry-related errors (150000-154999)
21    Entry,
22    /// Working copy errors (155000-159999)
23    WorkingCopy,
24    /// Filesystem backend errors (160000-164999)
25    Filesystem,
26    /// Repository errors (165000-169999)
27    Repository,
28    /// Repository access layer errors (170000-174999)
29    RepositoryAccess,
30    /// DAV protocol errors (175000-179999)
31    RaDav,
32    /// Local repository access errors (180000-184999)
33    RaLocal,
34    /// Diff algorithm errors (185000-189999)
35    Svndiff,
36    /// Apache module errors (190000-194999)
37    ApacheMod,
38    /// Client operation errors (195000-199999)
39    Client,
40    /// Miscellaneous errors including cancellation (200000-204999)
41    Misc,
42    /// Command-line client errors (205000-209999)
43    CommandLine,
44    /// SVN protocol errors (210000-214999)
45    RaSvn,
46    /// Authentication errors (215000-219999)
47    Authentication,
48    /// Authorization errors (220000-224999)
49    Authorization,
50    /// Diff operation errors (225000-229999)
51    Diff,
52    /// Serf/HTTP errors (230000-234999)
53    RaSerf,
54    /// Internal malfunction errors (235000-239999)
55    Malfunction,
56    /// X.509 certificate errors (240000-244999)
57    X509,
58    /// Unknown or APR error
59    Other,
60}
61
62// Errors are a bit special; they own their own pool, so don't need to use PooledPtr
63/// Represents a Subversion error.
64///
65/// SVN errors can form chains where each error points to a child error that provides
66/// more context. The lifetime parameter tracks ownership of the error chain:
67///
68/// - `Error<'static>` owns its error pointer and will free the entire chain on drop
69/// - `Error<'a>` borrows from another error's chain and shares the pointer without owning it
70///
71/// # Examples
72///
73/// Creating a simple error:
74/// ```
75/// use subversion::Error;
76///
77/// let err = Error::from_message("Something went wrong");
78/// ```
79///
80/// Checking error details:
81/// ```
82/// # use subversion::Error;
83/// # let err = Error::from_message("Something went wrong");
84/// println!("Error code: {}", err.code());
85/// println!("Error message: {}", err.message());
86/// println!("Error category: {:?}", err.category());
87/// ```
88///
89/// Traversing an error chain:
90/// ```
91/// # use subversion::Error;
92/// # let err = Error::from_message("Something went wrong");
93/// let mut current = Some(&err);
94/// while let Some(e) = current {
95///     println!("Error: {}", e.message());
96///     current = e.child().as_ref();
97/// }
98/// ```
99pub struct Error<'a> {
100    ptr: *mut svn_error_t,
101    owns_ptr: bool,
102    _phantom: std::marker::PhantomData<&'a ()>,
103}
104
105unsafe impl Send for Error<'_> {}
106
107impl Error<'static> {
108    /// Creates a new error with the given status, optional child error, and message.
109    pub fn new(status: apr::Status, child: Option<Error<'static>>, msg: &str) -> Self {
110        let msg = std::ffi::CString::new(msg).unwrap();
111        let child = child
112            .map(|mut e| unsafe { e.detach() })
113            .unwrap_or(std::ptr::null_mut());
114        let err = unsafe { subversion_sys::svn_error_create(status as i32, child, msg.as_ptr()) };
115        Self {
116            ptr: err,
117            owns_ptr: true,
118            _phantom: std::marker::PhantomData,
119        }
120    }
121
122    /// Creates a new error with a raw APR/SVN status code.
123    ///
124    /// Use this when you need SVN-specific error codes (like `SVN_ERR_CANCELLED`)
125    /// that cannot be represented by `apr::Status`.
126    pub fn with_raw_status(status: i32, child: Option<Error<'static>>, msg: &str) -> Self {
127        let msg = std::ffi::CString::new(msg).unwrap();
128        let child = child
129            .map(|mut e| unsafe { e.detach() })
130            .unwrap_or(std::ptr::null_mut());
131        let err = unsafe { subversion_sys::svn_error_create(status, child, msg.as_ptr()) };
132        Self {
133            ptr: err,
134            owns_ptr: true,
135            _phantom: std::marker::PhantomData,
136        }
137    }
138
139    /// Creates a new error from a string message.
140    pub fn from_message(msg: &str) -> Error<'static> {
141        Self::new(apr::Status::from(1), None, msg)
142    }
143
144    /// Creates an error from a raw SVN error pointer, or Ok if null.
145    pub fn from_raw(err: *mut svn_error_t) -> Result<(), Error<'static>> {
146        if err.is_null() {
147            Ok(())
148        } else {
149            Err(Error {
150                ptr: err,
151                owns_ptr: true,
152                _phantom: std::marker::PhantomData,
153            })
154        }
155    }
156}
157
158impl<'a> Error<'a> {
159    /// Wraps a raw SVN error pointer without taking ownership.
160    ///
161    /// The caller remains responsible for freeing `err`; the returned `Error`
162    /// will NOT call `svn_error_clear` on drop.
163    ///
164    /// # Safety
165    ///
166    /// `err` must be a valid, non-null pointer that outlives the returned `Error<'a>`.
167    pub(crate) unsafe fn from_ptr_borrowed(err: *mut svn_error_t) -> Error<'a> {
168        debug_assert!(!err.is_null());
169        Error {
170            ptr: err,
171            owns_ptr: false,
172            _phantom: std::marker::PhantomData,
173        }
174    }
175}
176
177impl<'a> Error<'a> {
178    /// Gets the APR error status code.
179    ///
180    /// Note: SVN-specific error codes (like `SVN_ERR_CANCELLED`) are mapped to
181    /// `apr::Status::General` because they fall outside the standard APR status range.
182    /// Use [`raw_apr_err()`](Self::raw_apr_err) when you need to distinguish SVN error codes.
183    pub fn apr_err(&self) -> apr::Status {
184        unsafe { (*self.ptr).apr_err }.into()
185    }
186
187    /// Gets the raw APR/SVN error status code as an integer.
188    ///
189    /// Unlike [`apr_err()`](Self::apr_err), this preserves the full error code
190    /// including SVN-specific codes (e.g. `SVN_ERR_CANCELLED = 200015`).
191    pub fn raw_apr_err(&self) -> i32 {
192        unsafe { (*self.ptr).apr_err }
193    }
194
195    /// Gets the mutable raw pointer to the error.
196    pub fn as_mut_ptr(&mut self) -> *mut svn_error_t {
197        self.ptr
198    }
199
200    /// Gets the raw pointer to the error.
201    pub fn as_ptr(&self) -> *const svn_error_t {
202        self.ptr
203    }
204
205    /// Gets the line number where the error occurred.
206    pub fn line(&self) -> i64 {
207        unsafe { (*self.ptr).line.into() }
208    }
209
210    /// Gets the file name where the error occurred.
211    pub fn file(&self) -> Option<&str> {
212        unsafe {
213            let file = (*self.ptr).file;
214            if file.is_null() {
215                None
216            } else {
217                Some(std::ffi::CStr::from_ptr(file).to_str().unwrap())
218            }
219        }
220    }
221
222    /// Gets the file and line location where the error occurred.
223    pub fn location(&self) -> Option<(&str, i64)> {
224        self.file().map(|f| (f, self.line()))
225    }
226
227    /// Gets the child error, if any.
228    ///
229    /// The returned error has the same lifetime as this error (both are part of the same error chain).
230    /// The returned error does not own its pointer - the parent error owns the entire chain.
231    pub fn child(&self) -> Option<Error<'a>> {
232        unsafe {
233            let child = (*self.ptr).child;
234            if child.is_null() {
235                None
236            } else {
237                Some(Error {
238                    ptr: child,
239                    owns_ptr: false,
240                    _phantom: std::marker::PhantomData,
241                })
242            }
243        }
244    }
245
246    /// Gets the error message.
247    pub fn message(&self) -> Option<&str> {
248        unsafe {
249            let message = (*self.ptr).message;
250            if message.is_null() {
251                None
252            } else {
253                Some(std::ffi::CStr::from_ptr(message).to_str().unwrap())
254            }
255        }
256    }
257
258    /// Finds an error in the chain with the given status code.
259    ///
260    /// The returned error borrows from this error's chain and does not own its pointer.
261    pub fn find_cause(&self, status: apr::Status) -> Option<Error<'a>> {
262        unsafe {
263            let err = subversion_sys::svn_error_find_cause(self.ptr, status as i32);
264            if err.is_null() {
265                None
266            } else {
267                Some(Error {
268                    ptr: err,
269                    owns_ptr: false,
270                    _phantom: std::marker::PhantomData,
271                })
272            }
273        }
274    }
275
276    /// Removes tracing information from the error.
277    ///
278    /// The returned error borrows from this error's chain and does not own its pointer.
279    pub fn purge_tracing(&self) -> Error<'_> {
280        unsafe {
281            Error {
282                ptr: subversion_sys::svn_error_purge_tracing(self.ptr),
283                owns_ptr: false,
284                _phantom: std::marker::PhantomData,
285            }
286        }
287    }
288
289    /// Detaches the error, returning the raw pointer and preventing cleanup.
290    ///
291    /// # Safety
292    ///
293    /// The caller assumes responsibility for managing the returned pointer's lifetime
294    /// and ensuring it is properly freed using Subversion's error handling functions.
295    pub unsafe fn detach(&mut self) -> *mut svn_error_t {
296        let err = self.ptr;
297        self.ptr = std::ptr::null_mut();
298        err
299    }
300
301    /// Converts the error into a raw pointer, consuming self without cleanup.
302    ///
303    /// # Safety
304    ///
305    /// The caller assumes responsibility for managing the returned pointer's lifetime
306    /// and ensuring it is properly freed using Subversion's error handling functions.
307    pub unsafe fn into_raw(self) -> *mut svn_error_t {
308        let err = self.ptr;
309        std::mem::forget(self);
310        err
311    }
312
313    /// Detaches the error from its borrowed lifetime, returning an owned
314    /// `Error<'static>`.
315    ///
316    /// The underlying `svn_error_t` is heap-allocated by Subversion and not
317    /// tied to any Rust pool, so re-binding its lifetime to `'static` is sound.
318    /// Use this to propagate an error obtained from a borrowing API out of a
319    /// function that returns `Error<'static>`.
320    pub fn into_static(self) -> Error<'static> {
321        let ptr = self.ptr;
322        let owns = self.owns_ptr;
323        std::mem::forget(self);
324        Error {
325            ptr,
326            owns_ptr: owns,
327            _phantom: std::marker::PhantomData,
328        }
329    }
330
331    /// Gets the best available error message from the error chain.
332    pub fn best_message(&self) -> String {
333        let mut buf = [0; 1024];
334        unsafe {
335            let ret = subversion_sys::svn_err_best_message(self.ptr, buf.as_mut_ptr(), buf.len());
336            std::ffi::CStr::from_ptr(ret).to_string_lossy().into_owned()
337        }
338    }
339
340    /// Collect all messages from the error chain
341    pub fn full_message(&self) -> String {
342        let mut messages = Vec::new();
343        let mut current = self.ptr;
344
345        unsafe {
346            while !current.is_null() {
347                let msg = (*current).message;
348                if !msg.is_null() {
349                    let msg_str = std::ffi::CStr::from_ptr(msg).to_string_lossy();
350                    if !msg_str.is_empty() {
351                        messages.push(msg_str.into_owned());
352                    }
353                }
354                current = (*current).child;
355            }
356        }
357
358        if messages.is_empty() {
359            self.best_message()
360        } else {
361            messages.join(": ")
362        }
363    }
364
365    /// Returns the error category based on the SVN error code.
366    ///
367    /// This allows programmatic handling of different error types without
368    /// parsing error messages.
369    ///
370    /// # Example
371    ///
372    /// ```no_run
373    /// # use subversion::error::ErrorCategory;
374    /// # fn example() -> Result<(), subversion::Error> {
375    /// let mut ctx = subversion::client::Context::new()?;
376    /// match ctx.checkout("https://svn.example.com/repo", "/tmp/wc", None, true) {
377    ///     Ok(_) => println!("Success"),
378    ///     Err(e) => match e.category() {
379    ///         ErrorCategory::Authentication => println!("Authentication required"),
380    ///         ErrorCategory::Authorization => println!("Permission denied"),
381    ///         ErrorCategory::Io => println!("I/O error occurred"),
382    ///         _ => println!("Other error: {}", e),
383    ///     }
384    /// }
385    /// # Ok(())
386    /// # }
387    /// ```
388    pub fn category(&self) -> ErrorCategory {
389        use subversion_sys::*;
390        // Get the raw apr_status_t value directly, not the apr::Status enum discriminant
391        let code = unsafe { (*self.ptr).apr_err as u32 };
392        let category_size = SVN_ERR_CATEGORY_SIZE;
393
394        match code {
395            c if c >= SVN_ERR_BAD_CATEGORY_START
396                && c < SVN_ERR_BAD_CATEGORY_START + category_size =>
397            {
398                ErrorCategory::BadInput
399            }
400            c if c >= SVN_ERR_XML_CATEGORY_START
401                && c < SVN_ERR_XML_CATEGORY_START + category_size =>
402            {
403                ErrorCategory::Xml
404            }
405            c if c >= SVN_ERR_IO_CATEGORY_START
406                && c < SVN_ERR_IO_CATEGORY_START + category_size =>
407            {
408                ErrorCategory::Io
409            }
410            c if c >= SVN_ERR_STREAM_CATEGORY_START
411                && c < SVN_ERR_STREAM_CATEGORY_START + category_size =>
412            {
413                ErrorCategory::Stream
414            }
415            c if c >= SVN_ERR_NODE_CATEGORY_START
416                && c < SVN_ERR_NODE_CATEGORY_START + category_size =>
417            {
418                ErrorCategory::Node
419            }
420            c if c >= SVN_ERR_ENTRY_CATEGORY_START
421                && c < SVN_ERR_ENTRY_CATEGORY_START + category_size =>
422            {
423                ErrorCategory::Entry
424            }
425            c if c >= SVN_ERR_WC_CATEGORY_START
426                && c < SVN_ERR_WC_CATEGORY_START + category_size =>
427            {
428                ErrorCategory::WorkingCopy
429            }
430            c if c >= SVN_ERR_FS_CATEGORY_START
431                && c < SVN_ERR_FS_CATEGORY_START + category_size =>
432            {
433                ErrorCategory::Filesystem
434            }
435            c if c >= SVN_ERR_REPOS_CATEGORY_START
436                && c < SVN_ERR_REPOS_CATEGORY_START + category_size =>
437            {
438                ErrorCategory::Repository
439            }
440            c if c >= SVN_ERR_RA_CATEGORY_START
441                && c < SVN_ERR_RA_CATEGORY_START + category_size =>
442            {
443                ErrorCategory::RepositoryAccess
444            }
445            c if c >= SVN_ERR_RA_DAV_CATEGORY_START
446                && c < SVN_ERR_RA_DAV_CATEGORY_START + category_size =>
447            {
448                ErrorCategory::RaDav
449            }
450            c if c >= SVN_ERR_RA_LOCAL_CATEGORY_START
451                && c < SVN_ERR_RA_LOCAL_CATEGORY_START + category_size =>
452            {
453                ErrorCategory::RaLocal
454            }
455            c if c >= SVN_ERR_SVNDIFF_CATEGORY_START
456                && c < SVN_ERR_SVNDIFF_CATEGORY_START + category_size =>
457            {
458                ErrorCategory::Svndiff
459            }
460            c if c >= SVN_ERR_APMOD_CATEGORY_START
461                && c < SVN_ERR_APMOD_CATEGORY_START + category_size =>
462            {
463                ErrorCategory::ApacheMod
464            }
465            c if c >= SVN_ERR_CLIENT_CATEGORY_START
466                && c < SVN_ERR_CLIENT_CATEGORY_START + category_size =>
467            {
468                ErrorCategory::Client
469            }
470            c if c >= SVN_ERR_MISC_CATEGORY_START
471                && c < SVN_ERR_MISC_CATEGORY_START + category_size =>
472            {
473                ErrorCategory::Misc
474            }
475            c if c >= SVN_ERR_CL_CATEGORY_START
476                && c < SVN_ERR_CL_CATEGORY_START + category_size =>
477            {
478                ErrorCategory::CommandLine
479            }
480            c if c >= SVN_ERR_RA_SVN_CATEGORY_START
481                && c < SVN_ERR_RA_SVN_CATEGORY_START + category_size =>
482            {
483                ErrorCategory::RaSvn
484            }
485            c if c >= SVN_ERR_AUTHN_CATEGORY_START
486                && c < SVN_ERR_AUTHN_CATEGORY_START + category_size =>
487            {
488                ErrorCategory::Authentication
489            }
490            c if c >= SVN_ERR_AUTHZ_CATEGORY_START
491                && c < SVN_ERR_AUTHZ_CATEGORY_START + category_size =>
492            {
493                ErrorCategory::Authorization
494            }
495            c if c >= SVN_ERR_DIFF_CATEGORY_START
496                && c < SVN_ERR_DIFF_CATEGORY_START + category_size =>
497            {
498                ErrorCategory::Diff
499            }
500            c if c >= SVN_ERR_RA_SERF_CATEGORY_START
501                && c < SVN_ERR_RA_SERF_CATEGORY_START + category_size =>
502            {
503                ErrorCategory::RaSerf
504            }
505            c if c >= SVN_ERR_MALFUNC_CATEGORY_START
506                && c < SVN_ERR_MALFUNC_CATEGORY_START + category_size =>
507            {
508                ErrorCategory::Malfunction
509            }
510            c if c >= SVN_ERR_X509_CATEGORY_START
511                && c < SVN_ERR_X509_CATEGORY_START + category_size =>
512            {
513                ErrorCategory::X509
514            }
515            _ => ErrorCategory::Other,
516        }
517    }
518}
519
520/// Gets the symbolic name for an error status code.
521pub fn symbolic_name(status: apr::Status) -> Option<&'static str> {
522    unsafe {
523        let name = subversion_sys::svn_error_symbolic_name(status as i32);
524        if name.is_null() {
525            None
526        } else {
527            Some(std::ffi::CStr::from_ptr(name).to_str().unwrap())
528        }
529    }
530}
531
532/// Gets a human-readable error string for a status code.
533pub fn strerror(status: apr::Status) -> Option<&'static str> {
534    let mut buf = [0; 1024];
535    unsafe {
536        let name = subversion_sys::svn_strerror(status as i32, buf.as_mut_ptr(), buf.len());
537        if name.is_null() {
538            None
539        } else {
540            Some(std::ffi::CStr::from_ptr(name).to_str().unwrap())
541        }
542    }
543}
544
545impl Clone for Error<'static> {
546    fn clone(&self) -> Self {
547        unsafe {
548            Error {
549                ptr: subversion_sys::svn_error_dup(self.ptr),
550                owns_ptr: true,
551                _phantom: std::marker::PhantomData,
552            }
553        }
554    }
555}
556
557impl Drop for Error<'_> {
558    fn drop(&mut self) {
559        // Only free if we own the pointer and it's non-null
560        if self.owns_ptr && !self.ptr.is_null() {
561            unsafe { subversion_sys::svn_error_clear(self.ptr) }
562        }
563    }
564}
565
566impl std::fmt::Debug for Error<'_> {
567    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
568        writeln!(
569            f,
570            "{}:{}: {}",
571            self.file().unwrap_or("<unspecified>"),
572            self.line(),
573            self.message().unwrap_or("<no message>")
574        )?;
575        let mut n = self.child();
576        while let Some(err) = n {
577            writeln!(
578                f,
579                "{}:{}: {}",
580                err.file().unwrap_or("<unspecified>"),
581                err.line(),
582                err.message().unwrap_or("<no message>")
583            )?;
584            n = err.child();
585        }
586        Ok(())
587    }
588}
589
590impl std::fmt::Display for Error<'_> {
591    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
592        write!(f, "{}", self.full_message())
593    }
594}
595
596impl std::error::Error for Error<'_> {}
597
598impl From<std::io::Error> for Error<'static> {
599    fn from(err: std::io::Error) -> Self {
600        Error::new(apr::Status::from(err.kind()), None, &err.to_string())
601    }
602}
603
604impl From<Error<'_>> for std::io::Error {
605    fn from(err: Error) -> Self {
606        let errno = err.apr_err().raw_os_error();
607        errno.map_or(
608            std::io::Error::other(err.message().unwrap_or("Unknown error")),
609            std::io::Error::from_raw_os_error,
610        )
611    }
612}
613
614impl From<std::ffi::NulError> for Error<'static> {
615    fn from(err: std::ffi::NulError) -> Self {
616        Error::from_message(&format!("Null byte in string: {}", err))
617    }
618}
619
620impl From<std::str::Utf8Error> for Error<'static> {
621    fn from(err: std::str::Utf8Error) -> Self {
622        Error::from_message(&format!("UTF-8 encoding error: {}", err))
623    }
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn test_error_chain_formatting() {
632        // Create a chain of errors
633        let child_err = Error::from_message("Child error");
634        let parent_err = Error::new(apr::Status::from(1), Some(child_err), "Parent error");
635
636        let full_msg = parent_err.full_message();
637        assert!(full_msg.contains("Parent error"));
638        assert!(full_msg.contains("Child error"));
639        assert!(full_msg.contains(": ")); // Check for separator
640    }
641
642    #[test]
643    fn test_single_error_message() {
644        let err = Error::from_message("Single error");
645        assert_eq!(err.message(), Some("Single error"));
646
647        let full_msg = err.full_message();
648        assert!(full_msg.contains("Single error"));
649    }
650
651    #[test]
652    fn test_error_display() {
653        let err = Error::from_message("Display test error");
654        let display_str = format!("{}", err);
655        assert!(display_str.contains("Display test error"));
656    }
657
658    #[test]
659    fn test_error_from_raw_null() {
660        Error::from_raw(std::ptr::null_mut()).unwrap();
661    }
662
663    #[test]
664    fn test_error_category() {
665        use subversion_sys::*;
666
667        // Test various error category codes - create errors directly via C API
668        let io_err_ptr = unsafe {
669            subversion_sys::svn_error_create(
670                SVN_ERR_IO_CATEGORY_START as i32,
671                std::ptr::null_mut(),
672                b"I/O error\0".as_ptr() as *const i8,
673            )
674        };
675        let io_err = Error {
676            ptr: io_err_ptr,
677            owns_ptr: true,
678            _phantom: std::marker::PhantomData,
679        };
680        assert_eq!(io_err.category(), ErrorCategory::Io);
681
682        let auth_err_ptr = unsafe {
683            subversion_sys::svn_error_create(
684                SVN_ERR_AUTHN_CATEGORY_START as i32,
685                std::ptr::null_mut(),
686                b"Auth error\0".as_ptr() as *const i8,
687            )
688        };
689        let auth_err = Error {
690            ptr: auth_err_ptr,
691            owns_ptr: true,
692            _phantom: std::marker::PhantomData,
693        };
694        assert_eq!(auth_err.category(), ErrorCategory::Authentication);
695
696        let authz_err_ptr = unsafe {
697            subversion_sys::svn_error_create(
698                SVN_ERR_AUTHZ_CATEGORY_START as i32,
699                std::ptr::null_mut(),
700                b"Authz error\0".as_ptr() as *const i8,
701            )
702        };
703        let authz_err = Error {
704            ptr: authz_err_ptr,
705            owns_ptr: true,
706            _phantom: std::marker::PhantomData,
707        };
708        assert_eq!(authz_err.category(), ErrorCategory::Authorization);
709
710        let wc_err_ptr = unsafe {
711            subversion_sys::svn_error_create(
712                SVN_ERR_WC_CATEGORY_START as i32,
713                std::ptr::null_mut(),
714                b"WC error\0".as_ptr() as *const i8,
715            )
716        };
717        let wc_err = Error {
718            ptr: wc_err_ptr,
719            owns_ptr: true,
720            _phantom: std::marker::PhantomData,
721        };
722        assert_eq!(wc_err.category(), ErrorCategory::WorkingCopy);
723
724        let repos_err_ptr = unsafe {
725            subversion_sys::svn_error_create(
726                SVN_ERR_REPOS_CATEGORY_START as i32,
727                std::ptr::null_mut(),
728                b"Repos error\0".as_ptr() as *const i8,
729            )
730        };
731        let repos_err = Error {
732            ptr: repos_err_ptr,
733            owns_ptr: true,
734            _phantom: std::marker::PhantomData,
735        };
736        assert_eq!(repos_err.category(), ErrorCategory::Repository);
737
738        let misc_err_ptr = unsafe {
739            subversion_sys::svn_error_create(
740                SVN_ERR_MISC_CATEGORY_START as i32,
741                std::ptr::null_mut(),
742                b"Misc error\0".as_ptr() as *const i8,
743            )
744        };
745        let misc_err = Error {
746            ptr: misc_err_ptr,
747            owns_ptr: true,
748            _phantom: std::marker::PhantomData,
749        };
750        assert_eq!(misc_err.category(), ErrorCategory::Misc);
751    }
752
753    #[test]
754    fn test_error_location_returns_value() {
755        // Test that Error::location() returns actual location info when available
756        use subversion_sys::*;
757
758        // Create an error with location information
759        // Use a static string to avoid memory management issues with CString
760        static TEST_FILE: &[u8] = b"test_file.c\0";
761
762        let err_ptr = unsafe {
763            let err = svn_error_create(
764                SVN_ERR_IO_CATEGORY_START as i32,
765                std::ptr::null_mut(),
766                b"Test error\0".as_ptr() as *const i8,
767            );
768            // SVN errors typically have file/line information set by internal macros
769            // We simulate this by setting them directly using a static string
770            (*err).file = TEST_FILE.as_ptr() as *const i8;
771            (*err).line = 42;
772            err
773        };
774
775        let err = Error {
776            ptr: err_ptr,
777            owns_ptr: true,
778            _phantom: std::marker::PhantomData,
779        };
780
781        // Verify location() returns the correct file and line
782        let location = err.location();
783        assert!(
784            location.is_some(),
785            "Error with file/line should have location"
786        );
787        let (file, line) = location.unwrap();
788        assert_eq!(file, "test_file.c", "File name should match");
789        assert_eq!(line, 42, "Line number should match");
790    }
791
792    #[test]
793    fn test_error_category_boundary_conditions() {
794        // Test all error category ranges with boundary conditions to catch mutations
795        // that modify range checks (>=, <, +, -, &&, ||)
796        use subversion_sys::*;
797
798        // Helper to create an error with a specific error code
799        let make_error = |code: u32| -> Error<'static> {
800            let err_ptr = unsafe {
801                svn_error_create(
802                    code as i32,
803                    std::ptr::null_mut(),
804                    b"Test\0".as_ptr() as *const i8,
805                )
806            };
807            Error {
808                ptr: err_ptr,
809                owns_ptr: true,
810                _phantom: std::marker::PhantomData,
811            }
812        };
813
814        let category_size = SVN_ERR_CATEGORY_SIZE;
815
816        // Test BadInput category (first category)
817        assert_eq!(
818            make_error(SVN_ERR_BAD_CATEGORY_START).category(),
819            ErrorCategory::BadInput,
820            "Start of BadInput range"
821        );
822        assert_eq!(
823            make_error(SVN_ERR_BAD_CATEGORY_START + category_size - 1).category(),
824            ErrorCategory::BadInput,
825            "End of BadInput range"
826        );
827        assert_eq!(
828            make_error(SVN_ERR_BAD_CATEGORY_START - 1).category(),
829            ErrorCategory::Other,
830            "Just before BadInput range"
831        );
832
833        // Test Xml category
834        assert_eq!(
835            make_error(SVN_ERR_XML_CATEGORY_START).category(),
836            ErrorCategory::Xml,
837            "Start of Xml range"
838        );
839        assert_eq!(
840            make_error(SVN_ERR_XML_CATEGORY_START + category_size - 1).category(),
841            ErrorCategory::Xml,
842            "End of Xml range"
843        );
844        assert_eq!(
845            make_error(SVN_ERR_XML_CATEGORY_START + category_size).category(),
846            ErrorCategory::Io,
847            "Just after Xml range should be Io"
848        );
849
850        // Test Io category
851        assert_eq!(
852            make_error(SVN_ERR_IO_CATEGORY_START).category(),
853            ErrorCategory::Io,
854            "Start of Io range"
855        );
856        assert_eq!(
857            make_error(SVN_ERR_IO_CATEGORY_START + category_size - 1).category(),
858            ErrorCategory::Io,
859            "End of Io range"
860        );
861
862        // Test Stream category
863        assert_eq!(
864            make_error(SVN_ERR_STREAM_CATEGORY_START).category(),
865            ErrorCategory::Stream,
866            "Start of Stream range"
867        );
868        assert_eq!(
869            make_error(SVN_ERR_STREAM_CATEGORY_START + category_size - 1).category(),
870            ErrorCategory::Stream,
871            "End of Stream range"
872        );
873
874        // Test Node category
875        assert_eq!(
876            make_error(SVN_ERR_NODE_CATEGORY_START).category(),
877            ErrorCategory::Node,
878            "Start of Node range"
879        );
880        assert_eq!(
881            make_error(SVN_ERR_NODE_CATEGORY_START + category_size - 1).category(),
882            ErrorCategory::Node,
883            "End of Node range"
884        );
885
886        // Test Entry category
887        assert_eq!(
888            make_error(SVN_ERR_ENTRY_CATEGORY_START).category(),
889            ErrorCategory::Entry,
890            "Start of Entry range"
891        );
892        assert_eq!(
893            make_error(SVN_ERR_ENTRY_CATEGORY_START + category_size - 1).category(),
894            ErrorCategory::Entry,
895            "End of Entry range"
896        );
897
898        // Test WorkingCopy category
899        assert_eq!(
900            make_error(SVN_ERR_WC_CATEGORY_START).category(),
901            ErrorCategory::WorkingCopy,
902            "Start of WorkingCopy range"
903        );
904        assert_eq!(
905            make_error(SVN_ERR_WC_CATEGORY_START + category_size - 1).category(),
906            ErrorCategory::WorkingCopy,
907            "End of WorkingCopy range"
908        );
909
910        // Test Filesystem category
911        assert_eq!(
912            make_error(SVN_ERR_FS_CATEGORY_START).category(),
913            ErrorCategory::Filesystem,
914            "Start of Filesystem range"
915        );
916        assert_eq!(
917            make_error(SVN_ERR_FS_CATEGORY_START + category_size - 1).category(),
918            ErrorCategory::Filesystem,
919            "End of Filesystem range"
920        );
921
922        // Test Repository category
923        assert_eq!(
924            make_error(SVN_ERR_REPOS_CATEGORY_START).category(),
925            ErrorCategory::Repository,
926            "Start of Repository range"
927        );
928        assert_eq!(
929            make_error(SVN_ERR_REPOS_CATEGORY_START + category_size - 1).category(),
930            ErrorCategory::Repository,
931            "End of Repository range"
932        );
933
934        // Test RepositoryAccess category
935        assert_eq!(
936            make_error(SVN_ERR_RA_CATEGORY_START).category(),
937            ErrorCategory::RepositoryAccess,
938            "Start of RepositoryAccess range"
939        );
940        assert_eq!(
941            make_error(SVN_ERR_RA_CATEGORY_START + category_size - 1).category(),
942            ErrorCategory::RepositoryAccess,
943            "End of RepositoryAccess range"
944        );
945
946        // Test RaDav category
947        assert_eq!(
948            make_error(SVN_ERR_RA_DAV_CATEGORY_START).category(),
949            ErrorCategory::RaDav,
950            "Start of RaDav range"
951        );
952        assert_eq!(
953            make_error(SVN_ERR_RA_DAV_CATEGORY_START + category_size - 1).category(),
954            ErrorCategory::RaDav,
955            "End of RaDav range"
956        );
957
958        // Test RaLocal category
959        assert_eq!(
960            make_error(SVN_ERR_RA_LOCAL_CATEGORY_START).category(),
961            ErrorCategory::RaLocal,
962            "Start of RaLocal range"
963        );
964        assert_eq!(
965            make_error(SVN_ERR_RA_LOCAL_CATEGORY_START + category_size - 1).category(),
966            ErrorCategory::RaLocal,
967            "End of RaLocal range"
968        );
969
970        // Test Svndiff category
971        assert_eq!(
972            make_error(SVN_ERR_SVNDIFF_CATEGORY_START).category(),
973            ErrorCategory::Svndiff,
974            "Start of Svndiff range"
975        );
976        assert_eq!(
977            make_error(SVN_ERR_SVNDIFF_CATEGORY_START + category_size - 1).category(),
978            ErrorCategory::Svndiff,
979            "End of Svndiff range"
980        );
981
982        // Test ApacheMod category
983        assert_eq!(
984            make_error(SVN_ERR_APMOD_CATEGORY_START).category(),
985            ErrorCategory::ApacheMod,
986            "Start of ApacheMod range"
987        );
988        assert_eq!(
989            make_error(SVN_ERR_APMOD_CATEGORY_START + category_size - 1).category(),
990            ErrorCategory::ApacheMod,
991            "End of ApacheMod range"
992        );
993
994        // Test Client category
995        assert_eq!(
996            make_error(SVN_ERR_CLIENT_CATEGORY_START).category(),
997            ErrorCategory::Client,
998            "Start of Client range"
999        );
1000        assert_eq!(
1001            make_error(SVN_ERR_CLIENT_CATEGORY_START + category_size - 1).category(),
1002            ErrorCategory::Client,
1003            "End of Client range"
1004        );
1005
1006        // Test Misc category
1007        assert_eq!(
1008            make_error(SVN_ERR_MISC_CATEGORY_START).category(),
1009            ErrorCategory::Misc,
1010            "Start of Misc range"
1011        );
1012        assert_eq!(
1013            make_error(SVN_ERR_MISC_CATEGORY_START + category_size - 1).category(),
1014            ErrorCategory::Misc,
1015            "End of Misc range"
1016        );
1017
1018        // Test CommandLine category
1019        assert_eq!(
1020            make_error(SVN_ERR_CL_CATEGORY_START).category(),
1021            ErrorCategory::CommandLine,
1022            "Start of CommandLine range"
1023        );
1024        assert_eq!(
1025            make_error(SVN_ERR_CL_CATEGORY_START + category_size - 1).category(),
1026            ErrorCategory::CommandLine,
1027            "End of CommandLine range"
1028        );
1029
1030        // Test RaSvn category
1031        assert_eq!(
1032            make_error(SVN_ERR_RA_SVN_CATEGORY_START).category(),
1033            ErrorCategory::RaSvn,
1034            "Start of RaSvn range"
1035        );
1036        assert_eq!(
1037            make_error(SVN_ERR_RA_SVN_CATEGORY_START + category_size - 1).category(),
1038            ErrorCategory::RaSvn,
1039            "End of RaSvn range"
1040        );
1041
1042        // Test Authentication category
1043        assert_eq!(
1044            make_error(SVN_ERR_AUTHN_CATEGORY_START).category(),
1045            ErrorCategory::Authentication,
1046            "Start of Authentication range"
1047        );
1048        assert_eq!(
1049            make_error(SVN_ERR_AUTHN_CATEGORY_START + category_size - 1).category(),
1050            ErrorCategory::Authentication,
1051            "End of Authentication range"
1052        );
1053
1054        // Test Authorization category
1055        assert_eq!(
1056            make_error(SVN_ERR_AUTHZ_CATEGORY_START).category(),
1057            ErrorCategory::Authorization,
1058            "Start of Authorization range"
1059        );
1060        assert_eq!(
1061            make_error(SVN_ERR_AUTHZ_CATEGORY_START + category_size - 1).category(),
1062            ErrorCategory::Authorization,
1063            "End of Authorization range"
1064        );
1065
1066        // Test Diff category
1067        assert_eq!(
1068            make_error(SVN_ERR_DIFF_CATEGORY_START).category(),
1069            ErrorCategory::Diff,
1070            "Start of Diff range"
1071        );
1072        assert_eq!(
1073            make_error(SVN_ERR_DIFF_CATEGORY_START + category_size - 1).category(),
1074            ErrorCategory::Diff,
1075            "End of Diff range"
1076        );
1077
1078        // Test RaSerf category
1079        assert_eq!(
1080            make_error(SVN_ERR_RA_SERF_CATEGORY_START).category(),
1081            ErrorCategory::RaSerf,
1082            "Start of RaSerf range"
1083        );
1084        assert_eq!(
1085            make_error(SVN_ERR_RA_SERF_CATEGORY_START + category_size - 1).category(),
1086            ErrorCategory::RaSerf,
1087            "End of RaSerf range"
1088        );
1089
1090        // Test Malfunction category
1091        assert_eq!(
1092            make_error(SVN_ERR_MALFUNC_CATEGORY_START).category(),
1093            ErrorCategory::Malfunction,
1094            "Start of Malfunction range"
1095        );
1096        assert_eq!(
1097            make_error(SVN_ERR_MALFUNC_CATEGORY_START + category_size - 1).category(),
1098            ErrorCategory::Malfunction,
1099            "End of Malfunction range"
1100        );
1101
1102        // Test X509 category (last category)
1103        assert_eq!(
1104            make_error(SVN_ERR_X509_CATEGORY_START).category(),
1105            ErrorCategory::X509,
1106            "Start of X509 range"
1107        );
1108        assert_eq!(
1109            make_error(SVN_ERR_X509_CATEGORY_START + category_size - 1).category(),
1110            ErrorCategory::X509,
1111            "End of X509 range"
1112        );
1113        assert_eq!(
1114            make_error(SVN_ERR_X509_CATEGORY_START + category_size).category(),
1115            ErrorCategory::Other,
1116            "Just after X509 range"
1117        );
1118
1119        // Test Other category (out of all ranges)
1120        assert_eq!(
1121            make_error(0).category(),
1122            ErrorCategory::Other,
1123            "Zero should be Other"
1124        );
1125        assert_eq!(
1126            make_error(1000).category(),
1127            ErrorCategory::Other,
1128            "Small values should be Other"
1129        );
1130        assert_eq!(
1131            make_error(300000).category(),
1132            ErrorCategory::Other,
1133            "Values beyond all categories should be Other"
1134        );
1135    }
1136
1137    #[test]
1138    fn test_error_best_message_returns_actual_message() {
1139        // Test that best_message() returns the actual error message, not "xyzzy" or empty string
1140        use subversion_sys::*;
1141
1142        let err_ptr = unsafe {
1143            svn_error_create(
1144                SVN_ERR_IO_CATEGORY_START as i32,
1145                std::ptr::null_mut(),
1146                b"Specific error message\0".as_ptr() as *const i8,
1147            )
1148        };
1149        let err = Error {
1150            ptr: err_ptr,
1151            owns_ptr: true,
1152            _phantom: std::marker::PhantomData,
1153        };
1154
1155        let msg = err.best_message();
1156        assert!(!msg.is_empty(), "best_message should not be empty");
1157        assert_ne!(msg, "xyzzy", "best_message should not be 'xyzzy'");
1158        assert_eq!(
1159            msg, "Specific error message",
1160            "best_message should return exact message, got '{}'",
1161            msg
1162        );
1163    }
1164
1165    #[test]
1166    fn test_error_child_returns_none_when_no_child() {
1167        // Test that child() returns None when there is no child error
1168        use subversion_sys::*;
1169
1170        let err_ptr = unsafe {
1171            svn_error_create(
1172                SVN_ERR_IO_CATEGORY_START as i32,
1173                std::ptr::null_mut(),
1174                b"Error without child\0".as_ptr() as *const i8,
1175            )
1176        };
1177
1178        let err = Error {
1179            ptr: err_ptr,
1180            owns_ptr: true,
1181            _phantom: std::marker::PhantomData,
1182        };
1183
1184        // Test that child() returns None when there is no child
1185        let child = err.child();
1186        assert!(
1187            child.is_none(),
1188            "child() should return None when no child exists"
1189        );
1190    }
1191
1192    #[test]
1193    fn test_error_find_cause_returns_none_for_non_matching_status() {
1194        // Test that find_cause() returns None when no error matches the requested status
1195        use subversion_sys::*;
1196
1197        // Create an error with a specific status
1198        let err_ptr = unsafe {
1199            svn_error_create(
1200                SVN_ERR_IO_CATEGORY_START as i32,
1201                std::ptr::null_mut(),
1202                b"Test error\0".as_ptr() as *const i8,
1203            )
1204        };
1205        let err = Error {
1206            ptr: err_ptr,
1207            owns_ptr: true,
1208            _phantom: std::marker::PhantomData,
1209        };
1210
1211        // Create another error with a different status to use for searching
1212        let different_err_ptr = unsafe {
1213            svn_error_create(
1214                (SVN_ERR_CLIENT_CATEGORY_START + 100) as i32,
1215                std::ptr::null_mut(),
1216                b"Different error\0".as_ptr() as *const i8,
1217            )
1218        };
1219        let different_err = Error {
1220            ptr: different_err_ptr,
1221            owns_ptr: true,
1222            _phantom: std::marker::PhantomData,
1223        };
1224        let different_status = different_err.apr_err();
1225        // Detach so it doesn't get cleaned up before we use the status
1226        std::mem::forget(different_err);
1227
1228        // Search for a status that won't be found in the first error
1229        let found = err.find_cause(different_status);
1230
1231        assert!(
1232            found.is_none(),
1233            "find_cause() should return None when status doesn't match any error in chain"
1234        );
1235
1236        // Clean up the different_err
1237        unsafe {
1238            subversion_sys::svn_error_clear(different_err_ptr);
1239        }
1240    }
1241
1242    #[test]
1243    fn test_error_child_returns_actual_child() {
1244        // Test that child() returns the actual child error when present, not None
1245        use subversion_sys::*;
1246
1247        let child_err_ptr = unsafe {
1248            svn_error_create(
1249                SVN_ERR_IO_CATEGORY_START as i32,
1250                std::ptr::null_mut(),
1251                b"Child error\0".as_ptr() as *const i8,
1252            )
1253        };
1254
1255        let parent_err_ptr = unsafe {
1256            svn_error_create(
1257                SVN_ERR_CLIENT_CATEGORY_START as i32,
1258                child_err_ptr,
1259                b"Parent error\0".as_ptr() as *const i8,
1260            )
1261        };
1262
1263        let parent_err = Error {
1264            ptr: parent_err_ptr,
1265            owns_ptr: true,
1266            _phantom: std::marker::PhantomData,
1267        };
1268
1269        // Test that child() returns Some when there is a child
1270        let child = parent_err.child();
1271        assert!(
1272            child.is_some(),
1273            "child() should return Some when child exists"
1274        );
1275
1276        let child_err = child.unwrap();
1277        assert_eq!(
1278            child_err.category(),
1279            ErrorCategory::Io,
1280            "Child error should have Io category"
1281        );
1282        assert!(
1283            child_err.message().unwrap().contains("Child error"),
1284            "Child error should have correct message"
1285        );
1286    }
1287
1288    #[test]
1289    fn test_error_find_cause_returns_matching_error() {
1290        // Test that find_cause() returns the error with matching status, not None
1291        // We create an error chain and verify find_cause can locate specific errors
1292        let child_err = Error::from_message("Child error");
1293        let parent_status = apr::Status::from(12345);
1294        let parent_err = Error::new(parent_status, Some(child_err), "Parent error");
1295
1296        // find_cause should find itself when searching for its own status
1297        let found = parent_err.find_cause(parent_status);
1298        assert!(
1299            found.is_some(),
1300            "find_cause() should find error with matching status"
1301        );
1302
1303        let found_err = found.unwrap();
1304        assert_eq!(
1305            found_err.apr_err(),
1306            parent_status,
1307            "Found error should have correct status"
1308        );
1309    }
1310
1311    #[test]
1312    fn test_error_as_ptr_returns_actual_pointer() {
1313        // Test that as_ptr() returns the actual pointer, not Default::default() (null)
1314        use subversion_sys::*;
1315
1316        let err_ptr = unsafe {
1317            svn_error_create(
1318                SVN_ERR_IO_CATEGORY_START as i32,
1319                std::ptr::null_mut(),
1320                b"Test\0".as_ptr() as *const i8,
1321            )
1322        };
1323
1324        let err = Error {
1325            ptr: err_ptr,
1326            owns_ptr: true,
1327            _phantom: std::marker::PhantomData,
1328        };
1329
1330        let ptr = err.as_ptr();
1331        assert!(!ptr.is_null(), "as_ptr() should return non-null pointer");
1332        assert_eq!(
1333            ptr, err_ptr,
1334            "as_ptr() should return the actual error pointer"
1335        );
1336    }
1337
1338    #[test]
1339    fn test_error_as_mut_ptr_returns_actual_pointer() {
1340        // Test that as_mut_ptr() returns the actual pointer, not Default::default() (null)
1341        use subversion_sys::*;
1342
1343        let err_ptr = unsafe {
1344            svn_error_create(
1345                SVN_ERR_IO_CATEGORY_START as i32,
1346                std::ptr::null_mut(),
1347                b"Test\0".as_ptr() as *const i8,
1348            )
1349        };
1350
1351        let mut err = Error {
1352            ptr: err_ptr,
1353            owns_ptr: true,
1354            _phantom: std::marker::PhantomData,
1355        };
1356
1357        let ptr = err.as_mut_ptr();
1358        assert!(
1359            !ptr.is_null(),
1360            "as_mut_ptr() should return non-null pointer"
1361        );
1362        assert_eq!(
1363            ptr, err_ptr,
1364            "as_mut_ptr() should return the actual error pointer"
1365        );
1366    }
1367
1368    #[test]
1369    fn test_symbolic_name_returns_actual_names() {
1370        // Test that symbolic_name() returns actual error names for errors we create,
1371        // not "xyzzy", "", or always None
1372
1373        // Create an actual error and get its status code
1374        let err = Error::from_message("Test error");
1375        let status = err.apr_err();
1376
1377        // Get the symbolic name for this error
1378        let name = symbolic_name(status);
1379
1380        // For an actual error we created, symbolic_name should return a valid name or None
1381        // We can't assert Some because not all error codes have symbolic names,
1382        // but if it returns Some, it must be valid
1383        if let Some(name_str) = name {
1384            assert!(
1385                !name_str.is_empty(),
1386                "Symbolic name should not be empty if returned"
1387            );
1388            assert_ne!(name_str, "xyzzy", "Symbolic name should not be 'xyzzy'");
1389            assert!(
1390                name_str.starts_with("SVN_"),
1391                "Symbolic name should start with SVN_, got: {}",
1392                name_str
1393            );
1394        }
1395
1396        // Status 0 (SVN_NO_ERROR) always has a known symbolic name; asserting
1397        // this guards against symbolic_name() degenerating to always-None.
1398        assert_eq!(symbolic_name(0.into()), Some("SVN_NO_ERROR"));
1399        let _ = symbolic_name(999999.into());
1400    }
1401
1402    #[test]
1403    fn test_strerror_returns_actual_error_strings() {
1404        // Test that strerror() returns actual error strings for errors we create,
1405        // not "xyzzy", "", or always None
1406
1407        // Create an actual error and get its status code
1408        let err = Error::from_message("Test error");
1409        let status = err.apr_err();
1410
1411        // Get the error string for this error
1412        let err_str = strerror(status);
1413
1414        // strerror MUST return Some for our created error, not None
1415        // This catches the mutation that always returns None
1416        assert!(
1417            err_str.is_some(),
1418            "strerror() must return Some for a valid SVN error code, got None"
1419        );
1420
1421        let err_msg = err_str.unwrap();
1422        assert!(
1423            !err_msg.is_empty(),
1424            "Error string should not be empty if returned"
1425        );
1426        assert_ne!(err_msg, "xyzzy", "Error string should not be 'xyzzy'");
1427        assert!(
1428            err_msg.len() > 2,
1429            "Error string should be substantive, got: {}",
1430            err_msg
1431        );
1432
1433        // Test that the function doesn't panic with various inputs
1434        // (these may or may not return Some, so we just check they don't panic)
1435        let _ = strerror(0.into());
1436        let _ = strerror(999999.into());
1437    }
1438
1439    #[test]
1440    fn test_error_category_off_by_one_and_midrange() {
1441        // Test off-by-one boundary conditions and mid-range values to catch operator mutations
1442        // This catches mutations like >= -> <, < -> ==, + -> -, && -> ||
1443        use subversion_sys::*;
1444
1445        let make_error = |code: u32| -> Error<'static> {
1446            let err_ptr = unsafe {
1447                svn_error_create(
1448                    code as i32,
1449                    std::ptr::null_mut(),
1450                    b"Test\0".as_ptr() as *const i8,
1451                )
1452            };
1453            Error {
1454                ptr: err_ptr,
1455                owns_ptr: true,
1456                _phantom: std::marker::PhantomData,
1457            }
1458        };
1459
1460        let category_size = SVN_ERR_CATEGORY_SIZE;
1461
1462        // For each category, test: START-1, START, START+1, MID, END-1, END, END+1
1463        let categories = vec![
1464            (
1465                SVN_ERR_BAD_CATEGORY_START,
1466                ErrorCategory::BadInput,
1467                "BadInput",
1468            ),
1469            (SVN_ERR_XML_CATEGORY_START, ErrorCategory::Xml, "Xml"),
1470            (SVN_ERR_IO_CATEGORY_START, ErrorCategory::Io, "Io"),
1471            (
1472                SVN_ERR_STREAM_CATEGORY_START,
1473                ErrorCategory::Stream,
1474                "Stream",
1475            ),
1476            (SVN_ERR_NODE_CATEGORY_START, ErrorCategory::Node, "Node"),
1477            (SVN_ERR_ENTRY_CATEGORY_START, ErrorCategory::Entry, "Entry"),
1478            (
1479                SVN_ERR_WC_CATEGORY_START,
1480                ErrorCategory::WorkingCopy,
1481                "WorkingCopy",
1482            ),
1483            (
1484                SVN_ERR_FS_CATEGORY_START,
1485                ErrorCategory::Filesystem,
1486                "Filesystem",
1487            ),
1488            (
1489                SVN_ERR_REPOS_CATEGORY_START,
1490                ErrorCategory::Repository,
1491                "Repository",
1492            ),
1493            (
1494                SVN_ERR_RA_CATEGORY_START,
1495                ErrorCategory::RepositoryAccess,
1496                "RepositoryAccess",
1497            ),
1498            (SVN_ERR_RA_DAV_CATEGORY_START, ErrorCategory::RaDav, "RaDav"),
1499            (
1500                SVN_ERR_RA_LOCAL_CATEGORY_START,
1501                ErrorCategory::RaLocal,
1502                "RaLocal",
1503            ),
1504            (
1505                SVN_ERR_SVNDIFF_CATEGORY_START,
1506                ErrorCategory::Svndiff,
1507                "Svndiff",
1508            ),
1509            (
1510                SVN_ERR_APMOD_CATEGORY_START,
1511                ErrorCategory::ApacheMod,
1512                "ApacheMod",
1513            ),
1514            (
1515                SVN_ERR_CLIENT_CATEGORY_START,
1516                ErrorCategory::Client,
1517                "Client",
1518            ),
1519            (SVN_ERR_MISC_CATEGORY_START, ErrorCategory::Misc, "Misc"),
1520            (
1521                SVN_ERR_CL_CATEGORY_START,
1522                ErrorCategory::CommandLine,
1523                "CommandLine",
1524            ),
1525            (SVN_ERR_RA_SVN_CATEGORY_START, ErrorCategory::RaSvn, "RaSvn"),
1526            (
1527                SVN_ERR_AUTHN_CATEGORY_START,
1528                ErrorCategory::Authentication,
1529                "Authentication",
1530            ),
1531            (
1532                SVN_ERR_AUTHZ_CATEGORY_START,
1533                ErrorCategory::Authorization,
1534                "Authorization",
1535            ),
1536            (SVN_ERR_DIFF_CATEGORY_START, ErrorCategory::Diff, "Diff"),
1537            (
1538                SVN_ERR_RA_SERF_CATEGORY_START,
1539                ErrorCategory::RaSerf,
1540                "RaSerf",
1541            ),
1542            (
1543                SVN_ERR_MALFUNC_CATEGORY_START,
1544                ErrorCategory::Malfunction,
1545                "Malfunction",
1546            ),
1547            (SVN_ERR_X509_CATEGORY_START, ErrorCategory::X509, "X509"),
1548        ];
1549
1550        for (start, expected_cat, name) in categories {
1551            // Test START (should be in category)
1552            assert_eq!(
1553                make_error(start).category(),
1554                expected_cat,
1555                "{}: START should be in category",
1556                name
1557            );
1558
1559            // Test START + 1 (should be in category, catches >= -> > mutation)
1560            assert_eq!(
1561                make_error(start + 1).category(),
1562                expected_cat,
1563                "{}: START+1 should be in category",
1564                name
1565            );
1566
1567            // Test mid-range (should be in category)
1568            let mid = start + category_size / 2;
1569            assert_eq!(
1570                make_error(mid).category(),
1571                expected_cat,
1572                "{}: MID should be in category",
1573                name
1574            );
1575
1576            // Test END - 2 (should be in category)
1577            assert_eq!(
1578                make_error(start + category_size - 2).category(),
1579                expected_cat,
1580                "{}: END-2 should be in category",
1581                name
1582            );
1583
1584            // Test END - 1 (should be in category, catches < -> <= mutation)
1585            assert_eq!(
1586                make_error(start + category_size - 1).category(),
1587                expected_cat,
1588                "{}: END-1 (last valid) should be in category",
1589                name
1590            );
1591
1592            // Test END (should NOT be in category, catches < -> <= mutation)
1593            assert_ne!(
1594                make_error(start + category_size).category(),
1595                expected_cat,
1596                "{}: END should NOT be in category",
1597                name
1598            );
1599
1600            // Test START - 1 (should NOT be in category for most, catches >= -> > mutation)
1601            // Skip for first category since START-1 might underflow
1602            if start > 1000 {
1603                assert_ne!(
1604                    make_error(start - 1).category(),
1605                    expected_cat,
1606                    "{}: START-1 should NOT be in category",
1607                    name
1608                );
1609            }
1610        }
1611
1612        // Test value way below all categories
1613        assert_eq!(
1614            make_error(100).category(),
1615            ErrorCategory::Other,
1616            "Value below all categories should be Other"
1617        );
1618
1619        // Test value way above all categories
1620        assert_eq!(
1621            make_error(500000).category(),
1622            ErrorCategory::Other,
1623            "Value above all categories should be Other"
1624        );
1625
1626        // Test between categories (just after BadInput, should be Xml or Other)
1627        let between = SVN_ERR_BAD_CATEGORY_START + category_size;
1628        let between_cat = make_error(between).category();
1629        assert_ne!(
1630            between_cat,
1631            ErrorCategory::BadInput,
1632            "Value just after BadInput should not be BadInput"
1633        );
1634    }
1635
1636    #[test]
1637    fn test_raw_apr_err_preserves_svn_error_codes() {
1638        // SVN error codes like SVN_ERR_CANCELLED (200015) are not standard APR
1639        // status codes and get mapped to General by apr::Status::from().
1640        // raw_apr_err() must preserve the original code.
1641        let cancelled_code = subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32;
1642        let err = Error::with_raw_status(cancelled_code, None, "cancelled");
1643
1644        assert_eq!(err.raw_apr_err(), cancelled_code);
1645        // apr_err() loses the distinction — both map to General
1646        assert_eq!(err.apr_err(), apr::Status::General);
1647    }
1648
1649    #[test]
1650    fn test_with_raw_status_creates_distinguishable_errors() {
1651        let cancelled_code = subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32;
1652        let fs_not_found_code = subversion_sys::svn_errno_t_SVN_ERR_FS_NOT_FOUND as i32;
1653
1654        let err1 = Error::with_raw_status(cancelled_code, None, "cancelled");
1655        let err2 = Error::with_raw_status(fs_not_found_code, None, "not found");
1656
1657        // apr_err() would return General for both — indistinguishable
1658        assert_eq!(err1.apr_err(), err2.apr_err());
1659        // raw_apr_err() preserves the difference
1660        assert_ne!(err1.raw_apr_err(), err2.raw_apr_err());
1661        assert_eq!(err1.raw_apr_err(), cancelled_code);
1662        assert_eq!(err2.raw_apr_err(), fs_not_found_code);
1663    }
1664
1665    #[test]
1666    fn test_with_raw_status_message_and_child() {
1667        let child = Error::from_message("child error");
1668        let parent = Error::with_raw_status(200015, Some(child), "parent error");
1669
1670        assert_eq!(parent.message(), Some("parent error"));
1671        let full = parent.full_message();
1672        assert!(full.contains("parent error"));
1673        assert!(full.contains("child error"));
1674    }
1675}