Skip to main content

starnix_task_command/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![warn(missing_docs)]
6
7//! The `TaskCommand` type and associated functions.
8
9use flyweights::FlyByteStr;
10use fuchsia_rcu::RcuDroppable;
11use std::ops::Range;
12
13/// The command for a task.
14///
15/// Linux task commands are limited to 15 bytes, but Fuchsia allows longer names in places. It's
16/// useful to store longer names diagnostics and debugging information.
17#[derive(Clone, Eq, Hash, PartialEq)]
18pub struct TaskCommand {
19    name: FlyByteStr,
20    linux_name_range: Option<Range<usize>>,
21}
22
23// SAFETY: TaskCommand contains FlyByteStr (interned string without drop side effects) and Option<Range<usize>>.
24unsafe impl RcuDroppable for TaskCommand {}
25
26impl TaskCommand {
27    /// Create a new `TaskCommand` from a byte slice. The byte slice is truncated at the first null
28    /// byte if any.
29    pub fn new(name: &[u8]) -> Self {
30        let name = if let Some(idx) = memchr::memchr(b'\0', name) { &name[..idx] } else { name };
31        Self { name: FlyByteStr::new(name), linux_name_range: None }
32    }
33
34    /// Create a new `TaskCommand` from a path. The basename of the path is used as the name.
35    pub fn from_path_bytes(path: &[u8]) -> Self {
36        let basename =
37            if let Some(idx) = memchr::memrchr(b'/', path) { &path[idx + 1..] } else { path };
38        Self::new(basename)
39    }
40
41    /// Returns the name truncated to 15 bytes.
42    pub fn comm_name(&self) -> &[u8] {
43        let bytes = self.linux_name_bytes();
44        &bytes[..std::cmp::min(bytes.len(), 15)]
45    }
46
47    /// Returns the name as a 16-byte array, null-terminated if shorter than 16 bytes,
48    /// as expected by `prctl(PR_GET_NAME)`.
49    pub fn prctl_name(&self) -> [u8; 16] {
50        let mut name = [0u8; 16];
51        let comm = self.comm_name();
52        name[..comm.len()].copy_from_slice(comm);
53        name
54    }
55
56    /// Returns the entire name as a byte slice.
57    pub fn as_bytes(&self) -> &[u8] {
58        self.name.as_bytes()
59    }
60
61    /// Returns the Linux name as a byte slice, without truncation.
62    fn linux_name_bytes(&self) -> &[u8] {
63        if let Some(range) = &self.linux_name_range {
64            &self.name.as_bytes()[range.clone()]
65        } else {
66            self.name.as_bytes()
67        }
68    }
69
70    /// Tries to embed `other` as the Linux name within this command.
71    /// Returns a new `TaskCommand` if `other` is a substring of this command.
72    pub fn try_embed(&self, other: &TaskCommand) -> Option<Self> {
73        use bstr::ByteSlice;
74        self.name.as_bytes().find(other.linux_name_bytes()).map(|offset| Self {
75            name: self.name.clone(),
76            linux_name_range: Some(offset..offset + other.linux_name_bytes().len()),
77        })
78    }
79}
80
81impl Default for TaskCommand {
82    fn default() -> Self {
83        Self::new(b"")
84    }
85}
86
87impl std::fmt::Debug for TaskCommand {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        self.name.fmt(f)
90    }
91}
92
93impl std::fmt::Display for TaskCommand {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        self.name.fmt(f)
96    }
97}
98
99impl PartialOrd for TaskCommand {
100    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
101        Some(self.cmp(other))
102    }
103}
104
105impl Ord for TaskCommand {
106    /// This comparison ignores the linux rendering of the name and provides a total ordering
107    /// based on the full name.
108    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
109        self.name.cmp(&other.name)
110    }
111}
112
113impl Into<FlyByteStr> for TaskCommand {
114    fn into(self) -> FlyByteStr {
115        self.name
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_new() {
125        assert_eq!(TaskCommand::new(b"foo").as_bytes(), b"foo");
126        assert_eq!(TaskCommand::new(b"foo\0bar").as_bytes(), b"foo");
127    }
128
129    #[test]
130    fn test_from_path_bytes() {
131        assert_eq!(TaskCommand::from_path_bytes(b"/foo/bar").as_bytes(), b"bar");
132        assert_eq!(TaskCommand::from_path_bytes(b"bar").as_bytes(), b"bar");
133        assert_eq!(TaskCommand::from_path_bytes(b"/bar").as_bytes(), b"bar");
134    }
135
136    #[test]
137    fn test_comm_name() {
138        assert_eq!(TaskCommand::new(b"short").comm_name(), b"short");
139        assert_eq!(TaskCommand::new(b"0123456789abcdef").comm_name(), b"0123456789abcde");
140        assert_eq!(TaskCommand::new(b"0123456789abcdefg").comm_name(), b"0123456789abcde");
141    }
142
143    #[test]
144    fn test_prctl_name() {
145        assert_eq!(TaskCommand::new(b"short").prctl_name(), *b"short\0\0\0\0\0\0\0\0\0\0\0");
146        assert_eq!(TaskCommand::new(b"0123456789abcdef").prctl_name(), *b"0123456789abcde\0");
147        assert_eq!(TaskCommand::new(b"0123456789abcdefg").prctl_name(), *b"0123456789abcde\0");
148    }
149
150    #[test]
151    fn test_prctl_name_16_bytes() {
152        let name = b"0123456789abcdef"; // 16 bytes
153        assert_eq!(TaskCommand::new(name).prctl_name(), *b"0123456789abcde\0");
154        assert_eq!(TaskCommand::new(name).comm_name(), b"0123456789abcde"); // 15 bytes
155    }
156
157    #[test]
158    fn test_debug() {
159        assert_eq!(format!("{:?}", TaskCommand::new(b"foo")), "\"foo\"");
160    }
161
162    #[test]
163    fn test_display() {
164        assert_eq!(TaskCommand::new(b"foo").to_string(), "foo");
165    }
166
167    #[test]
168    fn test_sniffing() {
169        let argv0 = TaskCommand::new(b"/path/to/binary");
170        let short = TaskCommand::new(b"binary");
171        let embedded = argv0.try_embed(&short).expect("should embed");
172        assert_eq!(embedded.as_bytes(), b"/path/to/binary");
173        assert_eq!(embedded.comm_name(), b"binary");
174
175        let other = TaskCommand::new(b"other");
176        assert!(argv0.try_embed(&other).is_none());
177    }
178
179    #[test]
180    fn test_comm_name_sniffed() {
181        let long_argv0 = TaskCommand::new(b"/path/to/short_name_with_suffix");
182        let short_name = TaskCommand::new(b"short_name");
183        let embedded = long_argv0.try_embed(&short_name).expect("should embed");
184        // comm_name should be "short_name" (len 10), not truncated version of full path
185        assert_eq!(embedded.comm_name(), b"short_name");
186    }
187}